TensorFlow-js-学习手册-全-

TensorFlow.js 学习手册(全)

原文:Learning TensorFlow.js

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

人工智能和机器学习是革命性的技术,可以改变世界,但只有当有开发人员使用良好的 API 来利用这些技术带来的进步时,它们才能实现这一目标。

其中一项进展是在浏览器中运行机器学习模型的能力,赋予应用程序智能行为。

TensorFlow.js 的崛起告诉我,人工智能已经到来。它不再仅仅属于拥有超级计算机的数据科学家;现在,数百万日常使用 JavaScript 编码的开发人员也可以访问它。但存在一个差距。构建模型的工具和技术仍然主要掌握在了那些了解 Python、NumPy、图形处理单元(GPU)、数据科学、特征建模、监督学习、张量等许多奇怪而美妙术语的人手中,你可能并不熟悉!

Gant 在这本书中所做的是直奔主题,教会你需要了解的重要内容,同时让你牢牢地保持在网页开发人员的角色中,使用 JavaScript 和浏览器。他将向你介绍人工智能和机器学习的概念,重点关注它们如何在你关心的平台上使用。

经常听到开发人员问,当想要使用机器学习时,“我在哪里可以找到可以重复使用的东西?我不想成为一个机器学习工程师,只是为了弄清楚这些东西是否适合我!”

Gant 在这本书中回答了这个问题。你将发现可以从 TensorFlow Hub 中获取的预制模型。你还将学习如何站在巨人的肩膀上,从使用数百万数据项和成千上万小时训练构建的模型中选择部分,并看看如何从中学习并将其转移到你自己的模型中。然后,只需将其放入你的页面中,让 JavaScript 来完成剩下的工作!

开发人员问,“我如何在我关心的平台上使用机器学习,而无需进行大量的重新训练?”

这本书深入探讨了这一点——向你展示如何弥合 JavaScript 和使用 TensorFlow 训练的模型之间的差距。从原始数据和张量之间的数据转换到将输出概率解析为文本,这本书将引导你完成与你的网站紧密集成的步骤。

开发人员问我,“我想超越其他人的工作和简单的原型。作为一个网页开发人员,我能做到吗?”

再次强调。当你完成这本书时,你不仅会熟悉使用模型,而且 Gant 会为你提供创建模型所需的所有细节。你将学习如何训练复杂的模型,比如卷积神经网络,以识别图像的内容,并且你将在 JavaScript 中完成所有这些工作。

2020 年 10 月的一项调查显示,全球有 1240 万 JavaScript 开发人员。其他调查显示,全球约有 30 万人从事人工智能工作。凭借 TensorFlow.js 技术和本书中的技能,亲爱的 JavaScript 开发人员,你可以成为让人工智能有意义的一部分。而这本书是一个美妙、美妙的开始。

享受这段旅程!

劳伦斯·莫罗尼

2021 年 3 月

前言

"如果你选择不做决定,你仍然已经做出了选择。"

—Geddy Lee(Rush)

让我们开始吧。

事后诸葛亮。"当比特币价格为 X 时,我应该买一些"或者"如果我在创业公司 Y 变得出名之前申请了就好了"。世界上充满了定义我们的时刻,无论是好是坏。时间永远不会倒流,但它会在我们前行时回响我们年轻时的选择教训。你很幸运有这本书和这个时刻来做决定。

由于人工智能的出现,软件行业的基础正在发生变化。这些变化最终将由那些抓住机会并塑造明天世界的人来决定。机器学习是一次探索新可能性的冒险,当它与 JavaScript 的广泛应用结合在一起时,限制就消失了。

就像我在谈论人工智能时告诉我的听众的那样,"你不是为了到这一步才创造软件而到这一步的"。所以让我们开始吧,看看我们的想象力会带我们去哪里。

为什么选择 TensorFlow.js?

TensorFlow 是市场上最受欢迎的机器学习框架之一。它得到了谷歌顶尖人才的支持,负责为世界上许多最有影响力的公司提供动力。TensorFlow.js 是 TensorFlow 不可战胜的 JavaScript 框架,比所有竞争对手都更好。简而言之,如果你想要一个 JavaScript 框架的强大功能,只有一个选择可以做到。

谁应该阅读这本书?

两个主要人群将享受并从这本书的内容中受益:

JavaScript 开发者

如果你熟悉 JavaScript,但以前从未接触过机器学习,这本书将成为你的向导。它倚重于框架,让你积极参与实用和令人兴奋的创作。通过构建各种项目,你将通过实践经验理解机器学习的基础知识。虽然我们不会回避数学或更深层次的概念,但我们也不会过度复杂化体验。如果你在 JavaScript 中构建网站并想获得新的超能力,请阅读这本书。

人工智能专家

如果你熟悉 TensorFlow 甚至是线性代数的基本原理,这本书将为你提供无数例子,展示如何将你的技能应用到 JavaScript 中。在这里,你将找到各种核心概念在 TensorFlow.js 框架中的展示。这将使你能够将你的广泛知识应用到可以高效存在于客户端浏览器或物联网设备(IoT)等边缘设备上的媒介中。阅读这本书,学习如何将你的创作带到无数设备上,提供丰富的互动体验。

这本书需要一定程度上对现代 JavaScript 的阅读和理解。

书籍概述

在概述这本书时,我意识到我必须做出选择。我可以创建一个旋风般的冒险,涉及到机器学习的各种应用,并用小而具体的例子涉及每一个,或者我可以选择一个单一的路径,讲述概念的不断增长的故事。在征求我的朋友和追随者的意见后,很明显后者是必要的。为了让这本书保持理智并控制在一千页以下,我选择删除任何 JavaScript 框架,专注于一个实用的旅程,探索人工智能的视觉方面。

每一章都以问题和一个特定的挑战结束,供你测试自己的决心。章节挑战部分经过精心设计,以巩固 TensorFlow.js 的教训。

章节

第一章和第二章从核心概念和一个具体示例开始。这种阴阳方法反映了本书的教学风格。每一章都建立在前几章提到的教训、词汇和功能之上。

第三章至第七章让您具备理解和实施现有 AI 工具和数据的视野。您将能够创建令人印象深刻的库,并在由大量数据科学家创建的项目中使用模型。

第八章至第十一章开始让您在 TensorFlow.js 中具有创造力。您将能够在 JavaScript 中训练模型,我坚信这是整本书中最有趣和令人兴奋的部分之一。

第十二章是最后的挑战。最后一章提供了一个顶点项目,让您可以利用本书所提供的一切,并用自己的能力表达出来。

要点

阅读本书后,无论您之前的经验如何,您都将能够在 TensorFlow.js 中找到、实现、调整和创建机器学习模型。您将能够识别网站中机器学习的应用,然后跟进并实现该应用。

本书中使用的约定

本书使用以下印刷约定:

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

常量宽度

用于程序清单,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

常量宽度粗体

显示应由用户按字面输入的命令或其他文本。

常量宽度斜体

显示应替换为用户提供的值或由上下文确定的值的文本。

提示

此元素表示提示或建议。

注意

此元素表示一般说明。

警告

此元素表示警告或注意。

使用代码示例

补充材料(代码示例、练习等)可在https://github.com/GantMan/learn-tfjs下载。

如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com

本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在程序和文档中使用它。除非您复制了代码的大部分内容,否则无需联系我们以获得许可。例如,编写一个使用本书中几个代码块的程序不需要许可。销售或分发 O'Reilly 图书中的示例需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书中大量示例代码合并到产品文档中需要许可。

我们感谢,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“学习 TensorFlow.js by Gant Laborde (O'Reilly)。版权所有 2021 年 Gant Laborde,978-1-492-09079-3。”

如果您认为您使用的代码示例超出了合理使用范围或上述授权,请随时与我们联系permissions@oreilly.com

致谢

我要感谢编辑、制作人员和 O'Reilly 的员工,在写这本书时与他们合作是一种快乐。

当然,感谢这本书前言作者杰出的劳伦斯·莫罗尼。你一直是我的偶像和启发,我从你那里学到了很多,我将继续在你的机器学习课程和成就的影响下茁壮成长。

这本书的技术审阅者是我有幸合作过的最友好和最彻底的人之一。

劳拉·乌兹卡特吉

你友善和鼓舞人心的反馈让人陶醉。你的直觉帮助使这本书感觉正确。

维舍什·拉维·施里马利

你理解了我所有的笑话。显然你是一个聪明友好的人,以这种方式,你推动我变得更好。我感激你所有的建议和智慧。

杰森·梅斯

我在你被选为这本书的技术编辑之前就认识你了,所以我把你看作是朋友,也是团队合作伙伴,一起让这本书变得更好。你的反馈是细致、聪明且不可替代的。真诚地感谢你。

阿克塞尔·达米安·西罗塔

我可以感受到你在每一点反馈中给予的高度个人支持。你花时间既聪明又善良。你的艺术是这本书的一份礼物。

李·沃里克

你继续审查和挑战我在所有作品中变得更好。这本书是我成就的最新例子,得益于你的洞察力。

米歇尔·克罗宁

我们每次会面我都很享受。你是一种持续的快乐!谢谢你做你自己。你让这本书变得轻松。你是如何做到的,我永远不会知道。

特别感谢弗兰克·冯·霍芬三世,他问了所有正确的问题,并递给我一页又一页手写的反馈笔记(图 P-1)。你的才华和诚实让我在传递强大和鼓舞人心的信息时保持引导。

手写的反馈页面

图 P-1。弗兰克提供了令人赞叹的支持和反馈

最后,感谢我爱的家人,他们尽可能避免打扰我,但也知道何时需要一个好的打断。爱丽西亚,我的爱,你比我更了解我自己。当我需要继续前进时,你给我惊喜的咖啡,当是时候停下来休息时,你给我一杯烈酒。每天,我最好的一面都是因为有你。

第一章:AI 是魔法

“任何足够先进的技术都是不可区分的魔法。”

—亚瑟·克拉克

好吧,AI 并不是真正的魔法。事实上,AI 是超越其他技术的一步,以至于它感觉像魔法。解释一个有效的排序算法是很容易的,但深入研究智能本身会触及第三导轨,将我们全部带入一个全新的技术力量水平。这种指数级的能力提升是通过 TensorFlow.js 的智慧和才能实现的。

科学家和工程师们在 AI 领域已经重新创造了比喻性的轮子,并调整了控制它的机制。当我们在本书中深入研究 AI 时,我们将用 TensorFlow.js 灵活而坚固的框架掌握这些概念,并借此将我们的想法实现在 JavaScript 不断扩展的领域中。是的,JavaScript,世界上最流行的编程语言。

这里提供的概念和定义将为您提供技术射线视野。您将能够穿透首字母缩略词和流行词汇,看到并理解我们周围各个领域中不断出现的 AI 基础设施。AI 和机器学习概念将变得清晰,本章的定义可以作为识别核心原则的参考,这些原则将推动我们在 TensorFlow.js 的学术发展中启程。

我们将:

  • 澄清 AI 和智能的领域

  • 讨论机器学习的类型

  • 回顾和定义常见术语

  • 通过 TensorFlow.js 的视角审视概念

让我们开始吧!

提示

如果您已经熟悉 TensorFlow.js 和机器学习术语、哲学和基本应用,您可能希望直接跳转到第二章。

JavaScript 中的 AI 之路

TensorFlow.js,简单来说,是一个处理 JavaScript 中特定 AI 概念的框架。就是这样。幸运的是,您在正确的书籍中,处于历史上正确的时刻。AI 工业革命才刚刚开始。

当计算机出现时,一个拥有计算机的人可以在显著的规模上执行几乎不可能的任务。他们可以破解密码,从大量数据中瞬间提取信息,甚至像与另一个人玩游戏一样。一个人无法做到的事情不仅变得可能,而且变得司空见惯。自数字发明诞生以来,我们的目标一直是赋予计算机更多的能力。作为独立的人类,我们能够做任何事情,但不是所有事情。计算机以一种扩展我们限制的方式使我们获得了新的力量。我们中的许多人花费一生来磨练一些技能,而在这些技能中,甚至更少的人成为我们的专长。我们都在建立一生的成就,我们中的一些人在某一领域成为世界上最优秀的人,这种技能只能通过运气、信息和成千上万天的努力获得...直到现在。

AI 使我们能够跳到队伍的最前面;大胆地建造以前从未建造过的东西。每天我们都看到公司和研究人员一次又一次地迈出计算的飞跃。我们站在一个新行业的入口处,邀请我们参与世界将如何改变的过程。

您掌握着方向盘,驶向下一个大事件,而这本书就是您的方向盘。我们学习 AI 魔法的旅程只会受到您想象力的限制。借助启用 JavaScript 的机器学习,您可以利用摄像头、麦克风、即时更新、位置和其他物理传感器、服务和设备!

我相信你会问,“为什么以前 AI 没有做到这一点?为什么现在这很重要?”要理解这一点,你需要踏上人类对再现智能的探索之旅。

什么是智能?

关于思想的概念,尤其是通向机器智能的道路,可以写成一本又一本的书。就像所有哲学努力一样,关于智能的每一个具体陈述都可以在途中争论。你不需要对一切都有确定的了解,但我们需要了解 AI 的领域,这样我们才能理解我们是如何来到 TensorFlow.js 这本书中的。

诗人和数学家们几个世纪以来一直在说,人类的思想不过是预先存在的概念的组合。生命的出现被认为是一种类似神的设计;我们都只是由元素“制造”而成。希腊神话中有发明之神赫淮斯托斯创造了能行走和行动如士兵般的自动铜制机器人。基本上,这些就是第一批机器人。机器人和智能的概念已经根植于我们的基础,作为古代传说中的终极和神圣的工艺。巨大的活力战士塔洛斯被著名地编程来守护克里特岛。虽然没有实际的铜制机器人,但这个故事为机械的志向提供了动力。数百年来,机械古代一直被认为是通往看似人类“智能”的途径,几个世纪后,我们开始看到生活在模仿艺术。小时候,我记得去我当地的 Chuck E. Cheese,这是美国一家受欢迎的餐厅,为儿童提供动画音乐表演。我记得曾经相信,就在那一刻,每天播放的木偶驱动的电子音乐会是真实的。我被激发了,这种激发驱使科学家追逐智能。这种火花一直存在,通过故事、娱乐,现在又通过科学传递。

随着能够自主工作和智能地工作的机器的概念在历史上不断发展,我们努力定义这些概念实体。学者们继续研究推理和学习,并出版作品,同时保持他们的术语在“机器”和“机器人”的领域内。机械智能的模仿总是受限于速度和电力的缺乏。

智能的概念在数百年来一直固定在人类思想中,远远超出了机械结构的范围,直到最终机器的诞生,计算机。计算机的诞生与大多数机器一样,都是为整个设备的单一目的而生。随着计算机的出现,一个新术语出现了,用来说明智能的不断增长,这在很大程度上反映了人类的智力。术语 AI 代表人工智能,直到 20 世纪 50 年代才被创造出来。随着计算机变得通用,哲学和学科开始结合。模仿智能的概念从神话中跃入科学研究领域。每一种为人类而设计的电子测量设备都成为计算机的新感官器官,也是电子和智能科学的一个令人兴奋的机会。

在相对较短的时间内,我们已经有了与人类交互并模拟人类行为的计算机。对人类活动的模仿提供了一种我们愿意称之为“智能”的形式。人工智能是这些战略行动的总称,无论其复杂程度或技术水平如何。一台可以下井字棋的计算机不必赢才能被归类为 AI。AI 是一个低门槛,不应与人类的一般智能混淆。最简单的代码片段也可以是合法的 AI,而好莱坞电影中的有感情机器的末日也是 AI。

当使用术语 AI 时,它是指来自一种惰性和通常非生物的设备的智能的总称。无论术语的最低门槛是什么,人类,拥有一门研究和一个不断增长的实际用途,都有一个统一的术语和明确的目标。所有被衡量的都被管理,因此人类开始衡量、改进并竞相追求更高级的 AI。

AI 的历史

AI 框架最初非常具体,但今天不再是这样。正如您可能知道或不知道的那样,TensorFlow.js 作为一个框架的概念可以应用于音乐、视频、图像、统计数据以及我们可以收集的任何数据。但并非总是这样。AI 的实现最初是缺乏任何动态能力的特定领域代码。

互联网上有一些笑话流传,称 AI 只是一堆 IF/THEN 语句的集合,而在我看来,它们并不完全错误。正如我们已经提到的,AI 是对自然智能各种模仿的总称。即使是初学者程序员也是通过解决简单的 AI 问题来学习编程,比如Ruby Warrior。这些编程练习教授算法的基础知识,并且需要相对较少的代码。这种简单性的代价是,虽然它仍然是 AI,但它被困在模仿程序员智能的境地。

很长一段时间以来,实施 AI 的主要方法依赖于一个编写 AI 程序的人的技能和哲学,这些技能和哲学直接转化为代码,以便计算机执行指令。数字逻辑正在执行那些传达程序的人的人类逻辑。当然,这是创建 AI 的最大延迟。您需要一个知道如何制造懂得的机器的人,我们受到他们的理解和翻译能力的限制。硬编码的 AI 无法推断超出其指令。这很可能是将任何人类智慧转化为人工智能的最大障碍。如果您想教会机器下棋,您如何教它下棋理论?如果您想告诉程序区分猫和狗,对于幼儿来说是微不足道的事情,您甚至知道从哪里开始编写算法吗?

在 50 年代末和 60 年代初,教师的概念从人类转变为能够阅读原始数据的算法。阿瑟·塞缪尔在一个事件中创造了术语机器学习(ML),这个事件使 AI 摆脱了创作者的实际限制。一个程序可以根据数据增长并掌握程序员无法将其转化为代码或自己从未理解的概念。

利用数据来训练程序的应用或功能是一个令人兴奋的抱负。但在计算机需要整个房间而数据远非数字化的时代,这也是一个不可逾越的要求。几十年过去了,计算机才达到了模拟人类信息和架构能力的关键转折点。

在 2000 年代,机器学习研究人员开始使用图形处理单元(GPU)来绕过“CPU 和内存之间的孤立通道”,即冯·诺伊曼瓶颈。2006 年,杰弗里·辛顿等人利用数据和神经网络(我们将在下一节中介绍的概念)来理解模式,并让计算机读取手写数字。这是一个以前对于普通计算来说太不稳定和不完美的壮举。深度学习能够读取和适应手写的随机性,以正确识别字符的最先进水平超过 98%。在这些发表的论文中,数据作为训练代理的概念从学术界的发表作品跃升到现实。

当 Hinton 陷入制作一个从头开始的学术证明神经网络有效的困境时,“这是什么数字?”问题成为机器学习实践者的一个支柱。这个问题已经成为机器学习框架的关键简单示例之一。TensorFlow.js 有一个演示,可以在不到两分钟的时间内直接在您的浏览器中解决这个问题。借助 TensorFlow.js 的优势,我们可以轻松构建在网站、服务器和设备上无缝运行的高级学习算法。但这些框架实际上在做什么呢?

AI 的最大目标一直是接近甚至超越人类的能力,98%的手写识别准确率正是如此。Hinton 的研究引发了对这些高效机器学习方法的关注,并创造了行业术语如深度神经网络。我们将在下一节详细说明原因,但这是应用机器学习的开始,它开始蓬勃发展,并最终进入了像 TensorFlow.js 这样的机器学习框架。虽然新的基于机器的学习算法不断被创造出来,但一个灵感和术语的来源变得非常清晰。我们可以模拟我们内部的生物系统来创造出一些先进的东西。从历史上看,我们使用自己和我们大脑的皮层(我们大脑的一层)作为结构化训练和智能的灵感来源。

神经网络

深度神经网络的概念始终受到我们人类身体的启发。数字节点(有时称为感知器网络)模拟我们大脑中的神经元,并像我们自己的突触一样激活,以创建一种平衡的思维机制。这就是为什么神经网络被称为神经,因为它模拟了我们大脑的生物化学结构。许多数据科学家厌恶与人脑的类比,但它通常是合适的。通过连接数百万个节点,我们可以构建优雅的深度神经网络,这些网络是用于做出决策的优雅数字机器。

通过增加更多层的神经通路,我们到达了深度学习这个术语。深度学习是一个庞大的层层连接的隐藏节点。您会听到这些节点被称为神经元人工神经元单元,甚至感知器。术语的多样性证明了为机器学习做出贡献的广泛科学家的广泛性。

这整个学习领域只是 AI 的一部分。如果您一直在关注,人工智能有一个称为机器学习的子集或分支,在这个集合内,我们有深度学习的概念。深度学习主要是一类推动机器学习的算法,但不是唯一的一种。请参见图 1-1 以获得这些主要术语的视觉表示。

AI 水平

图 1-1. AI 子领域

就像人类一样,通过示例和数据来适当平衡和构建神经元的迭代教学或“训练”被用来。起初,这些神经网络通常是错误和随机的,但随着他们看到一个又一个数据示例,他们的预测能力“学习”。

但我们的大脑并不直接感知世界。就像计算机一样,我们依赖于被组织成连贯数据发送到我们大脑的电信号。对于计算机来说,这些电信号类似于张量,但我们将在第三章中更详细地介绍这一点。TensorFlow.js 体现了研究和科学家们已经确认的所有这些进步。所有这些帮助人体执行的技术都可以包装在一个优化的框架中,这样我们就可以利用几十年来受人体启发的研究。

例如,我们的视觉系统从视网膜开始,使用神经节将光感信息传递到大脑,以激活这些神经元。正如一些人从儿童生物学中记得的那样,我们的视觉中有一些盲点,技术上我们看到的一切都是颠倒的。信号并不是“原样”发送到我们的大脑。这个视觉系统内置了技术,我们在今天的软件中利用了这些技术。

虽然我们都很兴奋地迎接我们的 8K 分辨率电视,你可能认为我们的大脑和视觉仍然超出了现代计算能力,但情况并非总是如此。连接我们眼睛到大脑的视觉信号的神经通道只有大约 10 Mb 的带宽。这相当于上世纪 80 年代初的局域网连接。即使是流媒体宽带连接也需要比这更多的带宽。但我们却能瞬间和迅速地感知一切,对吧?那么这是怎么做到的?我们是如何在这种过时的硬件上获得优越的信号的?答案是我们的视网膜在将数据发送到我们深度连接的神经网络之前对数据进行了压缩和“特征化”。这就是我们开始在计算机上做的事情。

卷积神经网络(CNN)在视觉数据上的工作方式与我们的眼睛和大脑一起压缩和激活我们的神经通路的方式相同。您将在第十章中进一步了解并编写自己的 CNN。我们每天都在了解我们的工作方式,并将这数百万年的进化直接应用于我们的软件。虽然了解这些 CNN 如何工作对您很有好处,但自己编写它们太学术化了。TensorFlow.js 带有您需要处理图像的卷积层。这是利用机器学习框架的基本好处。

你可以花费数年时间阅读和研究使计算机视觉、神经网络和人类有效运作的所有独特技巧和黑客技术。但我们生活在一个这些根源已经有时间生长、分支并最终结果的时代:这些先进的概念是可访问的,并内置在我们周围的服务和设备中。

今天的人工智能

今天我们使用这些最佳实践与人工智能相结合,以增强机器学习。卷积用于边缘检测,对某些区域的关注超过其他区域,甚至为一个单一项目提供多摄像头输入,为我们提供了一个在云机器训练 AI 的光纤服务器农场中的预先处理的数据宝库。

2015 年,AI 算法开始在某些视觉任务中胜过人类。正如你可能在新闻中听说的那样,AI 已经在癌症检测方面超越了人类,甚至在识别法律缺陷方面超过了美国顶级律师。正如数字信息一样,AI 在几秒钟内完成了这些任务,而不是几小时。AI 的“魔力”令人惊叹。

人们一直在寻找将人工智能应用于他们的项目的新颖有趣的方法,甚至创造全新的行业。

AI 已被应用于:

  • 生成写作、音乐和视觉方面的新内容

  • 推荐有用的内容

  • 取代简单的统计模型

  • 从数据中推断法则

  • 可视化分类器和识别器

所有这些突破都是深度学习的方面。今天我们拥有必要的硬件、软件和数据,可以通过深度机器学习网络实现突破性的变革。每天,社区和《财富》500 强公司都在人工智能领域发布新的数据集、服务和架构突破。

凭借您手头的工具和本书中的知识,您可以轻松地创造以前从未见过的东西,并将其带到网络上。无论是为了娱乐、科学还是财富,您都可以为任何现实世界问题或业务创建一个可扩展的智能解决方案。

如果说目前机器学习的问题是它是一种新的超级能力,而世界是广阔的。我们没有足够的例子来理解在 JavaScript 中拥有人工智能的全部好处。当电池寿命有了显著改善时,它们为更强大的手机和相机等设备带来了一个全新的世界,这些设备可以在单次充电的情况下持续数月。这一突破带来了数年内市场上无数新产品。机器学习不断取得突破,带来了新技术的飞速发展,我们甚至无法澄清或认识到这种洪流的加速。本书将重点介绍具体和抽象的例子,以便您可以使用 TensorFlow.js 应用实用解决方案。

为什么选择 TensorFlow.js?

您有选择。您可以从头开始编写自己的机器学习模型,也可以选择各种编程语言中的任何现有框架。即使在 JavaScript 领域,已经存在竞争框架、示例和选项。是什么使 TensorFlow.js 能够处理和承载今天的人工智能?

重要支持

TensorFlow.js 由 Google 创建和维护。我们将在第二章中更多地介绍这一点,但值得注意的是,世界上一些最优秀的开发人员已经齐心协力使 TensorFlow.js 成为可能。这也意味着,即使没有社区的努力,TensorFlow.js 也能够与最新和最伟大的突破性发展一起工作。

与其他基于 JavaScript 的机器学习库和框架实现不同,TensorFlow.js 支持经过优化和测试的 GPU 加速代码。这种优化传递给您和您的机器学习项目。

在线就绪

大多数机器学习解决方案都限制在一个非常定制的机器上。如果您想要创建一个网站来分享您的突破性技术,人工智能通常被锁在 API 后面。虽然在 Node.js 中运行 TensorFlow.js 是完全可行的,但在浏览器中直接运行 TensorFlow.js 也是可行的。这种无需安装的体验在机器学习领域是罕见的,这使您能够无障碍地分享您的创作。您可以版本化并访问丰富的互动世界。

离线就绪

JavaScript 的另一个好处是它可以在任何地方运行。代码可以保存到用户的设备上,如渐进式 Web 应用程序(PWA)、Electron 或 React Native 应用程序,然后可以在没有任何互联网连接的情况下始终如一地运行。不言而喻的是,与托管的 AI 解决方案相比,这也提供了显著的速度和成本提升。在本书中,您将发现许多完全存在于浏览器中的例子,这些例子可以使您和您的用户免受延迟和托管成本的困扰。

隐私

人工智能可以帮助用户识别疾病、税收异常和其他个人信息。通过互联网发送敏感数据可能存在危险。设备上的结果保留在设备上。甚至可以训练一个人工智能并将结果存储在用户的设备上,而没有任何信息离开浏览器的安全性。

多样性

应用 TensorFlow.js 在机器学习领域和平台上具有强大而广泛的影响。TensorFlow.js 可以利用 Web Assembly 在 CPU 或 GPU 上运行,适用于性能更强大的机器。如今的机器学习 AI 领域的光谱对于新手来说是一个重要而庞大的新术语和复杂性的世界。拥有一个可以处理各种数据的框架是有用的,因为它保持了您的选择。

精通 TensorFlow.js 使您能够将您的技能应用于支持 JavaScript 的各种平台(请参阅图 1-2)。

TFJS 的多平台演示。

图 1-2. TensorFlow.js 平台

使用 TensorFlow.js,您可以自由选择、原型设计和部署您的技能到各种领域。为了充分利用您的机器学习自由度,您需要了解一些术语,这些术语可以帮助您进入机器学习领域。

机器学习类型

许多人将机器学习分为三类,但我认为我们需要将所有机器学习视为四个重要元素:

  • 监督

  • 无监督

  • 半监督

  • 强化

每个元素都值得写成一本书。接下来的简短定义只是为了让您熟悉这个领域中会听到的术语。

快速定义:监督学习

在这本书中,我们将重点放在最常见的机器学习类别上,即监督机器学习(有时简称为监督学习或简称为监督)。监督机器学习简单地意味着我们对用于训练机器的每个问题都有一个答案。换句话说,我们的数据是有标签的。因此,如果我们试图教会机器区分一张照片是否包含鸟类,我们可以立即根据 AI 的答案是对还是错来评分。就像使用 Scantron 一样,我们有答案。但与 Scantron 不同的是,由于这是概率数学,我们还可以确定答案有多错。

如果 AI 对一张鸟类照片有 90%的确定性,虽然它回答正确,但还可以提高 10%。这突显了 AI 的“训练”方面,即通过即时数据驱动的满足感。

提示

如果你没有成百上千个现成的标记问题和答案,也不用担心。在这本书中,我们要么为您提供标记数据,要么向您展示如何自己生成数据。

快速定义:无监督学习

无监督学习不需要我们有答案。我们只需要问题。无监督机器学习是理想的,因为世界上大多数信息都没有标签。这类机器学习侧重于机器可以从未标记数据中学习和报告的内容。虽然这个主题可能有点令人困惑,但人类每天都在执行它!例如,如果我给你一张我的花园照片,并问你我拥有多少种不同类型的植物,你可以告诉我答案,而不必知道每株植物的属种。这有点类似于我们如何理解自己的世界。很多无监督学习都集中在对大量数据进行分类上。

快速定义:半监督学习

大多数时候,我们并不是在处理 100%未标记的数据。回到之前的花园例子,你可能不知道每株植物的属种,但也不完全无法对植物进行分类为 A 和 B。你可能告诉我,我有十株植物,其中三株是花,七株是草本植物。拥有少量已知标签可以帮助很多,当今的研究在半监督突破方面取得了巨大进展!

您可能听说过术语生成网络生成对抗网络(GANs)。这些流行的 AI 构造在许多 AI 新闻文章中被提及,并源自半监督学习策略。生成网络是根据我们希望网络创建的示例进行训练的,通过半监督方法,可以构建新的示例。生成网络非常擅长从少量标记数据中创建新内容。流行的 GAN 的例子通常有自己的网站,比如https://thispersondoesnotexist.com,越来越受欢迎,创意人员正在享受半监督输出带来的乐趣。

注意

GAN 在生成新内容方面发挥了重要作用。虽然流行的 GAN 是半监督的,但 GAN 的更深层概念并不局限于半监督网络。人们已经将 GAN 调整为适用于我们定义的每种学习类型。

快速定义:强化学习

解释强化学习最简单的方法是展示它在处理更真实的活动时是必需的,而不是之前的假设构造。

例如,如果我们在下棋时,我开始游戏时移动一个兵,那是一个好动作还是坏动作?或者如果我想让一个机器人把球踢进篮筐,它开始迈步,这是好还是坏?就像对待人类一样,答案取决于结果。这是为了最大奖励而采取的一系列动作,不总是有一个动作会产生一个结果。训练机器人首先迈步或首先看很重要,但可能不如在其他关键时刻所做的事情重要。而这些关键时刻都是由奖励作为强化来驱动的。

如果我要教一个 AI 玩《超级马里奥兄弟》,我想要高分还是快速胜利?奖励教会 AI 什么组合动作是最优化的以实现目标。强化学习(RL)是一个不断发展的领域,经常与其他形式的人工智能结合以培养最大的结果。

信息过载

对于刚提到的机器学习的许多应用,感到惊讶是可以的。在某种程度上,这就是为什么我们需要像 TensorFlow.js 这样的框架。我们甚至无法理解所有这些奇妙系统的用途及其未来几十年的影响!在我们理解这一切的同时,人工智能和机器学习的时代已经到来,我们将成为其中的一部分。监督学习是进入人工智能所有好处的一个很好的第一步。

我们将一起探讨一些最令人兴奋但实用的机器学习用途。在某些方面,我们只会触及表面,而在其他方面,我们将深入探讨它们的工作原理。以下是我们将涵盖的一些广泛类别。这些都是监督学习概念:

  • 图像分类

  • 自然语言处理(NLP)

  • 图像分割

本书的主要目标之一是,虽然你可以理解这些类别的概念,但不会受到限制。我们将倾向于实验和实践科学。有些问题可以通过过度工程解决,而有些问题可以通过数据工程解决。AI 和机器学习的思维是看到、识别和创建新工具的关键。

人工智能无处不在

我们正在进入一个人工智能渗透到一切的世界。我们的手机现在已经加速到深度学习硬件。摄像头正在应用实时人工智能检测,而在撰写本文时,一些汽车正在街上行驶,没有人类驾驶员。

在过去的一年中,我甚至注意到我的电子邮件已经开始自动写作,提供了一个“按 Tab 键完成”选项来完成我的句子。这个功能,比我最初写的任何东西更清晰更简洁,这是一个显著的可见成就,超过了多年来一直在同一个收件箱中保护我们免受垃圾邮件的被遗忘的机器学习 AI。

随着每个机器学习计划的展开,新的平台变得需求量大。我们正在将模型推向边缘设备,如手机、浏览器和硬件。寻找新语言来传承是合理的。很快搜索就会以 JavaScript 为明显选择。

框架提供的一次导览

机器学习是什么样子?以下是一个准确的描述,会让博士生因其简洁而感到不安。

在正常的代码中,人类直接编写代码,计算机读取和解释该代码,或者读取其某种派生形式。现在我们处于一个人类不编写算法的世界,那么实际发生了什么?算法从哪里来?

这只是一个额外的步骤。一个人编写算法的训练器。在框架的帮助下,甚至从头开始,一个人在代码中概述了问题的参数、所需的结构和要学习的数据的位置。现在,机器运行这个程序训练程序,不断地编写一个不断改进的算法作为解决问题的解决方案。在某个时候,您停止这个程序,取出最新的算法结果并使用它。

就是这样!

算法比用于创建它的数据要小得多。数千兆字节的电影和图像可以用来训练一个机器学习解决方案数周,所有这些只是为了创建几兆字节的数据来解决一个非常具体的问题。

最终的算法本质上是一组数字,这些数字平衡了人类程序员所确定的结构。这组数字及其相关的神经图通常被称为模型

您可能已经在技术文章中看到这些图表,它们被绘制为从左到右的一组节点,就像图 1-3。

神经网络

图 1-3. 密集连接神经网络的示例

我们的框架 TensorFlow.js 处理了指定模型结构或架构的 API,加载数据,通过我们的机器学习过程传递数据,并最终调整机器以更好地预测下次给定输入的答案。这就是 TensorFlow.js 真正的好处所在。我们只需要担心正确调整框架以解决问题,并保存生成的模型。

什么是模型?

当您在 TensorFlow.js 中创建一个神经网络时,它是所需神经网络的代码表示。该框架为每个神经元智能选择随机值生成一个图。此时,模型的文件大小通常是固定的,但内容会发展。当通过将数据传递给一个未经训练的具有随机值的网络进行预测时,我们得到的答案通常与正确答案相去甚远,就像纯随机机会一样。我们的模型没有经过任何数据训练,所以在工作中表现糟糕。因此,作为开发人员,我们编写的代码是完整的,但未经训练的结果很差。

一旦训练迭代发生了相当长的一段时间,神经网络的权重就会被评估然后调整。速度,通常被称为学习率,会影响结果。在以学习率为步长进行数千次这样的小步骤后,我们开始看到一个不断改进的机器,我们正在设计一个成功概率远远超出原始机器的模型。我们已经摆脱了随机性,收敛于使神经网络工作的数字!分配给给定结构中的神经元的那些数字就是训练好的模型。

TensorFlow.js 知道如何跟踪所有这些数字和计算图,所以我们不必,它还知道如何以适当和可消费的格式存储这些信息。

一旦您获得了这些数字,我们的神经网络模型就可以停止训练,只用来进行预测。在编程术语中,这已经变成了一个简单的函数。数据进去,数据出来。

通过神经网络传递数据看起来很像机会,如图 1-4 所示,但在计算世界中,这是一个平衡概率和排序的精密机器,具有一致和可重复的结果。数据被输入到机器中,然后得到一个概率性的结果。

神经网络分类

图 1-4. 一个平衡的网络隐喻

在下一章中,我们将尝试导入并使用一个完全训练好的模型进行预测。我们将利用数小时的训练来在微秒内得到智能分析。

在本书中

本书的结构使您可以将其收起放在度假中,一旦找到您的小天堂,就可以跟着书本阅读,学习概念,并审查答案。图像和截图应该足以解释 TensorFlow.js 的深层原理。

然而,要真正掌握这些概念,您需要超越简单地阅读本书。随着每个概念的展开,您应该编写代码,进行实验,并在实际计算机上测试 TensorFlow.js 的边界。对于那些作为机器学习领域的新手的人来说,重要的是您巩固您第一次看到的术语和工作流程。花时间逐步学习本书中的概念和代码。

相关代码

在整本书中,有可运行的源代码来说明 TensorFlow.js 的课程和功能。在某些情况下提供整个源代码,但在大多数情况下,打印的代码将仅限于重要部分。建议您立即下载与本书相匹配的源代码。即使您计划在示例旁边从头编写代码,您可能会遇到的小配置问题已经在相关代码中得到解决并可引用。

您可以在https://github.com/GantMan/learn-tfjs看到 GitHub 源页面。

如果您对 GitHub 和 Git 不熟悉,可以简单地下载最新的项目源代码的单个 ZIP 文件并引用它。

您可以从https://github.com/GantMan/learn-tfjs/archive/master.zip下载源 ZIP 文件。

源代码结构化以匹配每个章节。您应该能够在具有相同名称的文件夹中找到所有章节资源。在每个章节文件夹中,您将找到最多四个包含课程信息的文件夹。当您运行第一个 TensorFlow.js 代码时,将在第二章中进行审查。现在,请熟悉每个文件夹的目的,以便选择最适合您学习需求的示例代码。

额外文件夹

这个文件夹包含章节中引用的任何额外材料,包括文档或其他参考资料。这些部分的材料是每章的有用文件。

节点文件夹

这个文件夹包含了章节代码的 Node.js 特定实现,用于基于服务器的解决方案。该文件夹可能包含其中几个特定项目。Node.js 项目将安装一些额外的软件包,以简化实验过程。本书的示例项目使用以下内容:

nodemon

Nodemon 是一个实用程序,将监视源中的任何更改并自动重新启动服务器。这样可以保存文件并立即查看其相关更新。

ts-node

TypeScript 有很多选项,最明显的是强类型。然而,为了易于理解,本书专注于 JavaScript 而不是 TypeScript。ts-node模块用于支持 ECMAScript。您可以在这些节点示例中编写现代 JavaScript 语法,通过 TypeScript,代码将正常工作。

这些依赖项在package.json文件中标识。Node.js 示例用于说明使用 TensorFlow.js 的服务器解决方案,通常不需要在浏览器中打开。

要运行这些示例,请使用 Yarn 或 Node Package Manager(NPM)安装依赖项,然后执行启动脚本:

# Install dependencies with NPM
$ npm i
# Run the start script to start the server
$ npm run start
# OR use yarn to install dependencies
$ yarn
# Run the start script to start the server
$ yarn start

启动服务器后,您将在终端中看到任何控制台日志的结果。查看结果后,您可以使用 Ctrl+C 退出服务器。

简单文件夹

这个文件夹将包含没有使用 NPM 的解决方案。所有资源都简单地放在独立的 HTML 文件中进行服务。这绝对是最简单的解决方案,也是最常用的。这个文件夹很可能包含最多的结果。

web 文件夹

如果您熟悉基于客户端的 NPM Web 应用程序,您将会对web文件夹感到舒适。这个文件夹很可能包含其中的几个具体项目。web文件夹示例是使用Parcel.js打包的。这是一个用于 Web 项目的快速多核打包工具。Parcel 提供热模块替换(HMR),因此您可以保存文件并立即看到页面反映您的代码更改,同时还提供友好的错误日志记录和访问 ECMAScript。

要运行这些示例,请使用 Yarn 或 NPM 安装依赖项,然后执行启动脚本:

# Install dependencies with NPM
$ npm i
# Run the start script to start the server
$ npm run start
# OR use yarn to install dependencies
$ yarn
# Run the start script to start the server
$ yarn start

运行打包程序后,将打开一个网页,使用您的默认浏览器访问该项目的本地 URL。

提示

如果项目使用像照片这样的资源,那么该项目的根文件夹中将存在一个credit.txt文件,以正确归功于摄影师和来源。

章节部分

每一章都从确定章节目标开始,然后立即深入讨论。在每一章的末尾,您将看到一个章节挑战,这是一个资源,让您立即应用您刚学到的知识。每个挑战的答案可以在附录 B 中找到。

最后,每一章都以一组发人深省的问题结束,以验证您是否已内化了本章的信息。建议您尽可能通过代码验证答案,但答案也会在附录 A 中提供给您。

常见的 AI/ML 术语

您可能会想,“为什么模型不只是称为函数?模型在编程中已经有了意义,不需要另一个!”事实是这源自机器学习起源的问题。原始数据问题根植于统计学。统计模型将模式识别为样本数据的统计假设,因此我们从这些示例的数学运算中得到的产品是一个机器学习模型。机器学习术语通常会大量反映发明它的科学家的领域和文化。

数据科学伴随着大量的数学术语。我们将在整本书中看到这一主题,并且我们将为每个术语找出原因。有些术语立即就有意义,有些与现有的 JavaScript 和框架术语冲突,有些新术语与其他新术语冲突!命名事物是困难的。我们将尽力以易记的方式解释一些关键术语,并在途中详细说明词源。TensorFlow 和 TensorFlow.js 文档为开发人员提供了大量新词汇。阅读以下机器学习术语,看看您是否能掌握这些基本术语。如果不能,没关系。随着我们的进展,您可以随时回到本章并参考这些定义。

训练

训练是通过让机器学习算法审查数据并改进其数学结构以使其在未来做出更好的预测的过程。

TensorFlow.js 提供了几种方法来训练和监控训练模型,无论是在机器上还是在客户端浏览器上。

例如,“请不要触碰我的电脑,它已经在我的最新的空气弯曲算法上训练了三天。”

训练集

有时被称为“训练数据”,这是您将向算法展示的数据,让它从中学习。你可能会想,“这不就是我们拥有的所有数据吗?”答案是“不是”。

通常,大多数机器学习模型可以从它们以前见过的示例中学习,但测试并不能保证我们的模型可以推广到识别它以前从未见过的数据。重要的是,我们用来训练人工智能的数据要与验证和核实分开。

例如,“我的模型一直将热狗识别为三明治,所以我需要向我的训练集中添加更多照片。”

测试集

为了测试我们的模型是否能够对从未见过的数据进行处理,我们必须保留一些数据进行测试,并且永远不让我们的模型从中学习。这通常被称为“测试集”或“测试数据”。这个集合帮助我们测试我们是否已经创建了一个可以推广到现实世界新问题的东西。测试集通常比训练集要小得多。

例如,“我确保测试集是我们试图训练模型解决的问题的一个很好的代表。”

验证集

即使您还没有达到需要它的水平,这个术语也是很重要的。正如您经常会听到的,训练有时可能需要几小时、几天甚至几周。启动一个长时间运行的过程,只是回来发现您构建了错误的结构,必须重新开始,这有点令人担忧!虽然在本书中我们可能不会遇到任何这些大规模训练的需求,但这些情况可能需要一组数据进行更快的测试。当这与您的训练数据分开时,它是用于验证的“留出法”。基本上,这是一种实践,在让您的模型在昂贵的基础设施上训练或花费更长时间之前,将一小部分训练数据保留下来进行验证测试。这种调整和验证是您的验证集。

有很多方法可以选择、切片、分层甚至折叠您的验证集。这涉及到一种超出本书范围的科学,但当您讨论、阅读和提升自己的大型数据集时,了解这些知识是很有用的。

TensorFlow.js 在训练过程中有整个训练参数,用于识别和绘制验证结果。

例如,“我已经划分了一个小的验证集,在构建模型架构时使用。”

张量

我们将在第三章中详细介绍张量,但值得注意的是,张量是优化的数据结构,允许 GPU 和 Web Assembly 加速巨大的人工智能/机器学习计算集。张量是数据的数值持有者。

例如,“我已经将您的照片转换为灰度张量,以查看我们可以获得什么样的速度提升。”

归一化

归一化是将输入值缩放到更简单领域的操作。当一切都变成数字时,数字的稀疏性和数量的差异可能会导致意想不到的问题。

例如,房屋的大小和房屋中的浴室数量都会影响价格,但它们通常以完全不同的数字单位进行测量。并非所有事物都以相同的度量标准来衡量,虽然人工智能可以适应这些模式中的波动,但一个常见的技巧是简单地将数据缩放到相同的小领域。这样可以让模型更快地训练并更容易地找到模式。

例如,“我已经对房价和浴室数量进行了一些归一化,这样我们的模型可以更快地找到两者之间的模式。”

数据增强

在照片编辑软件中,我们可以拍摄图像并操纵它们,使其看起来像完全不同环境中的同一物体。这种方法有效地创建了一张全新的照片。也许您想要将您的标志放在建筑物的一侧或者压印在名片上。如果我们试图检测您的标志,原始照片和一些编辑过的版本将有助于我们的机器学习训练数据。

通常情况下,我们可以从原始数据中创建符合我们模型目标的新数据。例如,如果我们的模型将被训练来检测人脸,一个人的照片和一个镜像的人的照片都是有效的,而且明显不同的照片!

TensorFlow.js 有专门用于数据增强的库。我们将在本书的后面看到增强的数据。

例如,“我们通过镜像所有南瓜进行了一些数据增强,以扩大我们的训练集。”

特征和特征化

我们之前提到过特征化,当我们谈到眼睛如何将最重要的信息发送到大脑时。我们在机器学习中也是这样做的。如果我们试图制作一个猜测房子价值的 AI,那么我们必须确定哪些输入是有用的,哪些输入是噪音。

房子上的数据不缺乏,从砖块的数量到装饰线。如果你经常看家装电视节目,你会知道识别房子的大小、年龄、浴室数量、厨房最后一次更新的日期和社区是明智的。这些通常是识别房价的关键特征,你会更关心提供给模型这些信息,而不是一些琐碎的东西。特征化是从所有可能的数据中选择这些特征作为输入的过程。

如果我们决定把所有可能的数据都放进去,我们就给了我们的模型找到新模式的机会,但代价是时间和精力。没有理由选择像草叶的数量、房子的气味或正午的自然光线这样的特征,即使我们有这些信息或者我们觉得这对我们很重要。

即使我们选择了我们的特征,仍然会有错误和异常值会减慢实用机器学习模型的训练。有些数据只会让预测模型更成功的指针移动,选择明智的特征会使快速训练的智能 AI。

例如,“我相当确定计算感叹号的数量是检测这些营销邮件的关键特征。”

章节回顾

在这一章中,我们已经掌握了总称 AI 的术语和概念。我们也触及了我们将在本书中涵盖的关键原则。理想情况下,你现在对机器学习中必不可少的术语和结构更有信心了。

复习问题

让我们花点时间确保你完全掌握了我们提到的概念。花点时间回答以下问题:

  1. 你能给出机器学习的充分定义吗?

  2. 如果一个人想到了一个机器学习项目的想法,但是他们没有标记的数据,你会推荐什么?

  3. 什么样的机器学习对打败你最喜欢的视频游戏有用?

  4. 机器学习是唯一的 AI 形式吗?

  5. 一个模型是否保存了用于使其工作的所有训练示例数据?

  6. 机器学习数据是如何分解的?

这些练习的解决方案可以在附录 A 中找到。

¹ 编程语言统计:https://octoverse.github.com

² 人工智能是由约翰·麦卡锡在 1956 年首次学术会议上创造的。

第二章:介绍 TensorFlow.js

“如果你的行动激励他人梦想更多,学习更多,

做更多,成为更多,你就是一个领导者。”

—约翰·昆西·亚当斯

我们已经稍微谈到了 TensorFlow.js 以及它的功能,但我们还没有真正深入探讨像 TensorFlow.js 这样的机器学习框架到底是什么。在本章中,我们将探讨机器学习框架的概念,然后迅速进入编写代码。我知道编写具有某种实际结果的代码很重要,所以在本章中,你最终将让你的计算机运行 TensorFlow.js 并产生结果。

我们将:

  • 看看 TensorFlow.js 的概念

  • 设置 TensorFlow.js

  • 运行一个 TensorFlow.js 模型包

  • 深入了解 AI 的工作原理

让我们从我们将使用的框架开始。

你好,TensorFlow.js

考虑到我们之前的章节讨论了古代哲学和机器学习作为一个领域的诞生,你会期望人工智能框架的历史可以追溯到上世纪 60 年代初。然而,人工智能长时间停滞不前,这段时间通常被称为“人工智能寒冬”。人工智能的概念受到怀疑和极端数学计算的困扰,因为当时可用的数据量很小。谁能责怪这些研究人员呢?今天大多数软件开发人员依赖于发布应用程序,而不是从头开始编写支持 GPU 的线性代数和微积分,构建自己的人工智能不应该是例外。幸运的是,由于谷歌 Brain 团队的一些开源贡献,我们有了选择。

当你开始学习机器学习时,会听到很多流行词。TensorFlow、TensorFlow Lite 和 TensorFlow.js 都可能被提到,对于大多数新手来说,这些术语的含义以及为什么会有三个都不清楚。现在,让我们暂时忽略“张量”这个术语,因为你在第一章中已经听过这个词,而且在接下来的章节中你会真正理解它。相反,让我们专注于定义 TensorFlow.js,以便我们可以使用它。

TensorFlow,没有任何额外的“.js”或“Lite”,是谷歌的第一个公开的机器学习框架;谷歌 Brain 团队于 2015 年底发布了它。这个框架专注于用 Python 在云端有效解决谷歌的机器学习问题。谷歌很快意识到将这个流行的框架推广到计算能力有限的物联网和移动设备上会有好处,这就需要对 TensorFlow 进行适应,这就是所谓的 TensorFlow Lite。这一成功的适应为将 TensorFlow 理念推广到其他语言铺平了道路。

你可能可以猜到接下来会发生什么。2018 年初,谷歌宣布了一个由谷歌支持的 JavaScript 导入机器学习框架 TensorFlow 的版本,称为 TensorFlow.js。这一新举措以全新的方式增强了 TensorFlow 的实用性。Daniel Smilkov、Nikhil Thorat 和 Shanqing Cai 是发布 TensorFlow.js 的团队的一部分。在TensorFlow 开发者峰会上,Smilkov 和 Thorat 使用计算机视觉和网络摄像头训练一个模型来控制吃豆人游戏。

正是在这一刻,“仅限 Python”的选项被从流行的人工智能框架选项中移除,神经网络可以有效地穿越 JavaScript 领域。如果你可以运行 JavaScript,你就可以运行由 TensorFlow.js ML 支持的人工智能。

这三种实现今天都是活跃的,并随着它们特定目的的增长。通过将 TensorFlow 扩展到 JavaScript 实现,我们现在可以在节点服务器甚至客户端浏览器中实现 AI/ML。在论文“TensorFlow.js: 用于 Web 和更多的机器学习”中(Daniel Smilkov 等人,2019),他们表示,“TensorFlow.js 使来自庞大 JavaScript 社区的新一代开发人员能够构建和部署机器学习模型,并实现新类的设备上计算。” TensorFlow.js 可以利用广泛的设备平台,同时仍然可以访问 GPU 甚至 Web Assembly。有了 JavaScript,我们的机器学习可以涉足地平线并回来。

值得注意的是,在几项基准测试中,Node 在较低的 CPU 负载下胜过了 Python 3,因此尽管 Python 一直是大多数 AI 的采用语言,JavaScript 作为产品和服务的主要语言平台。

但没有必要删除或推广任何一种语言。TensorFlow 模型基于有向无环图(DAG),这是与语言无关的图,是训练的输出。这些图可以由一种语言训练,然后转换并被完全不同的编程语言消耗。本书的目标是为您提供使用 JavaScript 和 TensorFlow.js 的工具,以便充分利用它们。

利用 TensorFlow.js

对于很多人来说,“学习”有时可能意味着从基础开始,这意味着从数学开始。对于这些人来说,像 TensorFlow 这样的框架和像 TensorFlow.js 这样的实用分支是一个糟糕的开始。在本书中,我们将构建项目,并涉及 TensorFlow.js 框架的基础知识,我们将很少或根本不花时间在底层数学魔法上。

像 TensorFlow 和 TensorFlow.js 这样的框架帮助我们避免涉及的线性代数的具体细节。您不再需要关注前向传播反向传播这样的术语,以及它们的计算和微积分。相反,我们将专注于像推断模型训练这样的行业术语。

虽然 TensorFlow.js 可以访问底层 API(如tfjs-core)来对经典问题进行一些基本优化,但这些时刻留给了那些无论手头的框架如何都有坚实基础的学者和高级用户。这本书旨在展示 TensorFlow.js 的强大之处,利用框架的辛勤工作和优化是我们将如何做到这一点。我们让 TensorFlow.js 负责配置和优化我们的代码,以适应各种设备约束和 WebGL API。

我们甚至可能走得太远,将机器学习应用于您可以轻松手工编码的算法,但这通常是大多数人真正理解概念的地方。用机器学习解决您理解的简单问题有助于您推断解决您无法手工编码的高级问题的步骤、逻辑和权衡。

另一方面,一些关于神经元、激活函数和模型初始化的基础知识是不能被忽视的,可能需要一些解释。本书的目标是为您提供理论和实用性的健康平衡。

正如您可能已经推测到的那样,TensorFlow.js 的各种平台意味着没有单一的预设设置。我们可以在本书中在客户端或服务器上运行 TensorFlow.js。然而,我们最隐性的交互选项是充分利用浏览器。因此,我们将在浏览器中执行大部分示例。当然,在适当的情况下,我们仍将涵盖托管节点服务器解决方案的关键方面。这两种工具都有各自的优缺点,我们将在探索 TensorFlow.js 的强大之处时提到。

让我们准备好 TensorFlow.js

与任何流行工具一样,您可能会注意到 TensorFlow.js 包有几种不同版本,以及几个可以访问代码的位置。本书的大部分内容将专注于 TensorFlow.js 最常用和“准备运行”的版本,即浏览器客户端。优化的框架版本是为服务器端制作的。这些版本与 Python 使用相同的底层 C++核心 API 进行通信,但通过 Node.js,这使您能够利用服务器的图形卡或 CPU 的所有性能。TensorFlow.js AI 模型在各种位置运行,并利用各种环境的各种优化(请参见图 2-1)。

Tensorflow 选项图表

图 2-1。TensorFlow.js 的选项

本书中学到的知识可以应用于大多数平台。为了方便起见,我们将覆盖最常见平台的设置过程。如果您不愿意从头开始设置环境,可以直接访问与本书相关的源代码中为您构建的预配置项目,位于https://github.com/GantMan/learn-tfjs

在浏览器中设置 TensorFlow.js

让我们来看看运行 TensorFlow.js 的最快、最多功能和最简单的方法。要在浏览器中运行 TensorFlow.js,实际上非常容易。我假设您熟悉 JavaScript 的基础知识,并且以前已经将 JavaScript 库导入到现有代码中。TensorFlow.js 支持多种包含方式,因此任何经验的开发人员都可以访问它。如果您熟悉包含 JavaScript 依赖项,您将熟悉这些常见做法。我们可以以两种方式将 TensorFlow.js 导入到页面中:

  • 使用 NPM

  • 包含脚本标签

使用 NPM

管理网站依赖项的最流行方式之一是使用包管理器。如果您习惯使用 NPM 或 Yarn 构建项目,您可以通过 NPM 注册表访问代码https://oreil.ly/R2lB8。只需在命令行安装依赖项:

# Import with npm
$ npm i @tensorflow/tfjs

# Or Yarn
$ yarn add @tensorflow/tfjs

导入tfjs包后,您可以在 JavaScript 项目中使用以下 ES6 JavaScript 导入代码导入此代码:

import * as tf from '@tensorflow/tfjs';

包含脚本标签

如果网站不使用包管理器,您可以简单地向 HTML 文档添加一个脚本标签。这是您可以在项目中包含 TensorFlow.js 的第二种方式。您可以下载并在本地托管 TensorFlow.js,或者利用内容传送网络(CDN)。我们将把脚本标签指向 CDN 托管的脚本源:

<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.7.0/dist/tf.min.js">
</script>

除了跨网站缓存外,CDN 非常快,因为它们利用边缘位置确保全球快速交付。

注意

您可能已经注意到,我已将此代码锁定到特定版本(2.7.0),我强烈建议您在涉及 CDN 的项目中始终这样做。您不希望在网站出现自动破坏性更改的问题。

在 Node 中设置 TensorFlow.js

我们在浏览器中使用的 TensorFlow.js 包与 Node.js 完全兼容,如果您计划仅暂时尝试 Node.js,这是一个很好的解决方案。一个好的规则是,如果您不打算为他人托管实时项目或在大量数据上进行训练,则使用简单的/tfjs而不是/tfjs-node导入。

如果您的目标是超越实验,并在 TensorFlow.js 中实现有效的 Node.js,您应该花一些时间改进您的 Node.js 设置,使用一些这些替代包。有两个更好的 TensorFlow.js 分发版本专门为 Node 和速度构建。它们是tfjs-nodetfjs-node-gpu。请记住,每台开发者机器都是独特的,您的安装和体验可能有所不同。

对于 Node.js,您可能需要在@tensorflow/tfjs-node@tensorflow/tfjs-node-gpu之间进行选择。如果您的计算机配置了 NVIDIA GPU 并正确设置了 CUDA 软件,您可以使用后者 GPU 加速的软件包。Compute Unified Device Architecture(CUDA)允许通过并行计算平台直接访问 GPU 加速的 NVIDIA 硬件。虽然 GPU 软件包是 TensorFlow.js 选项中绝对最快的,但由于其硬件和软件约束,它也是最不可能准备好并配置好的软件包。目前,我们的示例将在安装tfjs-node上运行,并将可选的 CUDA 配置留给您。

# Import with npm
$ npm i @tensorflow/tfjs-node

# Or Yarn
$ yarn add @tensorflow/tfjs-node
警告

通常,如果您的计算机尚未设置为开发高级 C++库,您可能需要安装一些软件来准备好您的计算机。只有当您希望积极使用tfjs-nodetfjs-node-gpu时,才需要进行这种深入研究。

如果您的 NPM 安装成功,恭喜!您已准备好从此包中导入。如果您已经设置了 Node 来处理 ES6,您可以使用以下代码导入:

import * as tf from '@tensorflow/tfjs-node';

如果您尚未配置 Node.js 软件包以处理 ES6 导入,您仍然可以使用经典的 require 访问代码:

const tf = require('@tensorflow/tfjs-node');

验证 TensorFlow.js 是否正常工作

之前的所有方法都会在您的 JavaScript 代码中提供一个名为tf的变量,这使您可以访问 TensorFlow.js。为了确保我们的导入工作正常,让我们记录导入的 TensorFlow.js 库的版本。

将此代码添加到您的 JavaScript 中,如果在控制台中看到一个版本打印出来,您的导入就可以继续进行了!

console.log(tf.version.tfjs);

运行页面时,我们可以右键单击页面并检查以访问 JavaScript 控制台日志。在那里,我们将找到我们的日志命令的输出,“3.0.0”或您导入的 TensorFlow.js 版本。对于 Node.js 示例,该值将直接在控制台中打印。

警告

在访问tf变量(TensorFlow.js 库)的功能之前,通常需要确保 TensorFlow.js 已经正确加载了后端并准备就绪。上述代码绕过了这个检查,但始终最好运行您的初始代码等待tf.ready()的承诺。

下载并运行这些示例

如第一章中所述,您可以访问本书中的代码。为了确保您不必在每个示例中从头开始设置这些项目,请确保您拥有每个项目的源代码,包括之前显示的简单代码。

从书的存储库中以您喜欢的方式下载项目:https://github.com/GantMan/learn-tfjs

转到第二章的目录,并确保您可以在您的计算机上运行代码。

运行简单示例

chapter2/simple/simplest-example中,我们避免使用 NPM,只是从 CDN 中拉取我们的代码。以当前结构化的方式,我们甚至不需要托管网站!我们只需在任何现代浏览器中打开index.html,它就会运行!

在某个时候,我们实际上需要托管这些简单示例,因为我们将访问需要完整 URI 的其他资产。我们可以通过使用一个小型的 Web 服务器来托管文件来轻松实现这一点。我知道的最小的 Web 服务器叫做 Web 服务器,有一个有趣的手绘“200 OK!”标志。在五分钟内,我们就可以在本地服务器上正确提供我们的文件。

您可以在Chrome Web Store 作为扩展上找到 Chrome 的 Web 服务器。在本书中,我们有时会称此插件为“200 OK!”当您将 Web 服务器指向index.html文件时,它将自动为您提供文件,并且所有相邻文件都可以通过其关联的 URL 访问,正如我们将在后续课程中所需的那样。应用程序界面应该看起来像图 2-3。

200 OK!对话框

图 2-3。Chrome 的 Web 服务器 200 OK!对话框

如果您想查看其他选项或想要链接到提到的 Chrome 插件,请查看chapter2/extra/hosting-options.md,找到适合您的选项。当然,如果您发现一个未列出的绝妙选项,请贡献一个拉取请求。

一旦找到一个以您喜欢的方式运行simple-example的服务器,您可以将该服务用于以后的所有简单选项。

运行 NPM web 示例

如果您更熟悉 NPM,此项目的基本 NPM 示例使用 Parcel。Parcel 是最快的应用程序捆绑工具,零配置。它还包括热模块重新加载,以获取实时更新和出色的错误日志记录。

要运行代码,请导航至chapter2/web/web-example并执行 NPM 安装(npm i)。完成后,在package.json中有一个脚本可以启动所有内容。您只需运行启动脚本:

$ npm run start

就是这样!我们将使用这种方法来运行本书中所有基于 NPM 的代码。

运行 Node.js 示例

Node.js 示例与 Parcel NPM 示例一样易于运行。虽然 Node.js 通常没有明确的意见,但本书中的 Node.js 示例将包括一些明确的开发依赖项,以便我们可以使我们的 Node.js 示例代码与浏览器示例保持一致。本书中的代码将充分利用 ECMAScript。我们通过一些转译、文件监视和节点魔法来实现这一点。

为了准备这个示例,请导航至chapter2/node-example并执行 NPM 安装(npm i)。如果遇到任何问题,您可能需要运行npm i -g ts-node nodemon node-gyp来确保您拥有所需的库,以确保我们所有的魔法发生。一旦您的节点包正确放置,您可以随时通过运行启动脚本来启动项目:

$ npm run start

代码通过 TypeScript 转译,并且使用nodemon进行重新加载。如果一切正常运行,您将在运行服务器的控制台/终端中直接看到已安装的 TensorFlow.js 版本。

让我们使用一些真实的 TensorFlow.js

现在我们有了 TensorFlow.js,让我们用它来创造一些史诗般的东西!好吧,这有点简化:如果那么容易,这本书就结束了。仍然有很多东西要学习,但这并不妨碍我们乘坐缆车,以获得高层视角。

TensorFlow.js 有大量预先编写的代码和模型可供我们利用。这些预先编写的库帮助我们获得利用 TensorFlow.js 的好处,而无需完全掌握底层概念。

虽然有很多社区驱动的模型效果很好,但 TensorFlow.js 模型的官方维护列表在 TensorFlow GitHub 上,名称为tfjs-models。为了稳定性,我们将尽可能经常在本书中使用这些模型。您可以在这里查看链接:https://github.com/tensorflow/tfjs-models

在这次尝试运行实际 TensorFlow.js 模型时,让我们选择一个相对简单的输入和输出。我们将使用 TensorFlow.js 的Toxicity分类器来检查文本输入是否具有侮辱性。

毒性分类器

谷歌提供了几个不同复杂度的“即插即用”模型。其中一个有益的模型被称为毒性模型,这可能是对初学者来说最直接和有用的模型之一。

像所有编程一样,模型将需要特定的输入并提供特定的输出。让我们开始看看这个模型的输入和输出是什么。毒性检测有毒内容,如威胁、侮辱、咒骂和普遍仇恨。由于这些并不一定是互斥的,因此每种违规行为都有自己的概率是很重要的。

毒性模型试图识别给定输入是否符合以下特征的真假概率:

  • 身份攻击

  • 侮辱

  • 淫秽

  • 严重毒性

  • 性暴力

  • 威胁

  • 毒性

当您给模型一个字符串时,它会返回一个包含七个对象的数组,用于识别每个特定违规行为的概率预测百分比。百分比表示为两个介于零和一之间的Float32值。

如果一句话肯定是违规行为,概率将主要分配给Float32数组中的零索引。

例如,[0.7630404233932495, 0.2369595468044281]表示对于这种特定违规行为的预测是 76%不是违规行为,24%可能是违规行为。

对于大多数开发人员来说,这可能是一个“等等,什么!?”的时刻。在我们习惯于真和假的地方得到概率,这有点奇怪,不是吗?但直观地,我们一直知道语言有很多灰色地带。侮辱的确切科学往往取决于个人,甚至是当天!

因此,该模型具有一个额外功能,允许您传递一个阈值,当特定违规行为超过分配的限制时将其识别出来。当检测到超过阈值的侮辱时,match标志将设置为 true。这是一个很好的额外功能,可以帮助您快速映射重大违规行为的结果。选择有效的阈值取决于您的需求和情况。您可以凭直觉行事,但如果需要一些指导,统计学有各种工具供您查阅。阅读有关接收器操作特性(ROC)图的文章,以绘制和选择适合您需求的最佳阈值。

警告

要激活毒性模型,我们将不得不写一些侮辱性的话。以下示例使用基于外表的侮辱。这个侮辱避免使用粗话,但仍然是冒犯性的。这并不针对任何特定人,而是旨在说明 AI 理解和识别有毒评论的能力。

选择一个对人类容易识别但对计算机难以识别的侮辱是很重要的。在文本形式中检测讽刺是困难的,并且一直是计算机科学中的一个主要问题。为了严肃测试这个模型,侮辱应避免使用常见和明显的煽动性措辞。将阈值设置为0.5,在特别狡猾的侮辱上运行毒性模型会产生示例 2-1 中显示的数组。

侮辱输入:“她看起来像一个穴居人,只是远不如智慧!”

示例 2-1。输入句子的完整毒性报告
[{
      "label":"identity_attack",
      "results":[{
            "probabilities":{
               "0":0.9935033917427063,
               "1":0.006496586836874485
            }, "match":false
         }]
   },{
      "label":"insult",
      "results":[{
            "probabilities":{
               "0":0.5021483898162842,
               "1":0.4978516101837158
            }, "match":false
         }]
   },{
      "label":"obscene",
      "results":[{
            "probabilities":{
               "0":0.9993441700935364,
               "1":0.0006558519671671093
            }, "match":false
         }]
   },{
      "label":"severe_toxicity",
      "results":[{
            "probabilities":{
               "0":0.9999980926513672,
               "1":0.0000018614349528434104
            }, "match":false
         }]
   },{
      "label":"sexual_explicit",
      "results":[{
            "probabilities":{
               "0":0.9997043013572693,
               "1":0.00029564235592260957
            }, "match":false
         }]
   },{
      "label":"threat",
      "results":[{
            "probabilities":{
               "0":0.9989342093467712,
               "1":0.0010658185929059982
            }, "match":false
         }]
   },{
      "label":"toxicity",
      "results":[{
            "probabilities":{
               "0":0.4567308723926544,
               "1":0.543269157409668
            }, "match":true
         }]
}]

正如您从示例 2-1 中可以看到的,我们在“侮辱”雷达下勉强通过(50.2%错误),但我们被毒性指标扣分,导致"match": true。这相当令人印象深刻,因为我在句子中没有任何明确的冒犯性语言。作为程序员,编写一个算法来捕捉和识别这种有毒的侮辱并不直接,但 AI 经过研究大量标记的侮辱后,被训练来识别有毒语言的复杂模式,这样我们就不必自己做了。

前面的示例使用一个句子的数组作为输入。如果将多个句子作为输入,您的句子索引将直接对应于每个类别的结果索引。

但不要只听我的话;现在轮到您运行代码了。您可以通过以下方式将模型添加到您的网站:

$ npm install @tensorflow-models/toxicity

然后导入库:

import * as toxicity from "@tensorflow-models/toxicity";

或者您可以直接从 CDN 添加脚本。³ 脚本标签的顺序很重要,所以确保在尝试使用模型之前将标签放在页面上:

<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/toxicity@1.2.2">
</script>

前面的任何示例都将在一个准备就绪的toxicity变量中提供结果。我们将使用这个变量的load方法来加载 ML 模型的承诺。然后,我们可以利用该模型在一个句子数组上使用classify方法。

以下是加载模型并对三个句子进行分类的示例。这个确切的示例可以在GitHub 上的章节代码的相关部分中以三种不同形式找到。

// minimum positive prediction confidence
// If this isn't passed, the default is 0.85
const threshold = 0.5;

// Load the model // ①
toxicity.load(threshold).then((model) => {
  const sentences = [
    "You are a poopy head!",
    "I like turtles",
    "Shut up!"
  ];

  // Ask the model to classify inputs // ②
  model.classify(sentences).then((predictions) => {
    // semi-pretty-print results
    console.log(JSON.stringify(predictions, null, 2)); // ③
  });
});

模型加载到浏览器中并带有阈值。

加载的模型被要求对输入进行分类。

使用 JavaScript 对象表示法很好地打印了对象。

注意

如果您在浏览器中运行此代码,您需要查看控制台以查看输出。您可以通过检查页面导航到控制台,或者通常情况下,您可以在 Windows 上按 Control+Shift+J 或在 Mac 上按 Command+Option+J。如果您使用npm start从命令行运行此代码,您应该立即在控制台中看到输出。

多个句子的结果按毒性类别分组。因此,前面的代码尝试根据每个类别识别每个句子。例如,前面的“侮辱”输出应该类似于示例 2-2。

示例 2-2. 侮辱部分结果
  ...
  {
    "label": "insult",
    "results": [
      {
        "probabilities": {
          "0": 0.05905626341700554,
          "1": 0.9409437775611877
        },
        "match": true
      },
      {
        "probabilities": {
          "0": 0.9987999200820923,
          "1": 0.0012000907445326447
        },
        "match": false
      },
      {
        "probabilities": {
          "0": 0.029087694361805916,
          "1": 0.9709123373031616
        },
        "match": true
      }
    ]
  },
  ...

哒哒哒!代码运行得很好。每个results索引对应于输入句子索引,并且正确诊断了三个句子中的两个侮辱。

祝贺您运行您的第一个 TensorFlow.js 模型。现在您是 AI 的大师,让我们一起讨论这个库的步骤和基本概念。

加载模型

当我们调用toxicity.load时,您可能会认为模型被加载到内存中,但您只对一半正确。大多数这些库不会在 JavaScript 代码库中提供经过训练的模型。再读一遍那句话。这对于我们的 NPM 开发人员可能有点令人担忧,但对于我们的 CDN 用户来说完全合理。加载方法触发一个网络调用来下载库使用的模型。在某些情况下,加载的模型会针对 JavaScript 所在的环境和设备进行优化。查看图 2-4 中说明的网络日志。

毒性下载

图 2-4. 网络下载请求
警告

尽管毒性 NPM 捆绑包可以被缩小并压缩到仅 2.4 KB,但在使用库时,实际模型文件在网络上有额外的多兆字节负载。

这个毒性库的加载方法需要一个阈值,它将应用于所有后续分类,然后触发一个网络调用来下载实际的模型文件。当模型完全下载时,库会将模型加载到张量优化内存中供使用。

适当评估每个库是很重要的。让我们回顾一些人们在学习更多关于这个库时常问的常见问题。

分类

我们的毒性代码接下来做的事情是运行classify方法。这是我们的输入句子通过模型传递的时刻,我们得到了结果。虽然它看起来就像任何其他 JavaScript 函数一样简单,但这个库实际上隐藏了一些必要的基本处理。

模型中的所有数据都被转换为张量。我们将在第三章中更详细地介绍张量,但重要的是要注意,这种转换对于 AI 至关重要。所有输入字符串都被转换,进行计算,得到的结果是重新转换为普通 JavaScript 基元的张量。

很高兴这个库为我们处理了这个问题。当您完成本书时,您将能够以相同的灵活性包装机器学习模型。您将能够让您的用户对发生在幕后的数据转换的复杂性保持幸福的无知。

在下一章中,我们将深入探讨这种转换。您将完全掌握数据转换为张量以及随之而来的所有数据操作超能力。

自己试试

现在你已经实现了一个模型,很可能你可以实现谷歌提供的其他模型。大多数其他谷歌模型的 GitHub 页面都有 README 文档,解释如何实现每个库。许多实现与我们在毒性中看到的类似。

花点时间浏览现有的模型,让你的想象力发挥得淋漓尽致。你可以立即开始使用这些库进行工作。随着你在本书中的进展,了解这些模型的存在也会很有用。你不仅将更好地理解这些库的能力,还可能想要结合甚至改进这些现有库以满足你的需求。

在下一章中,我们将开始深入挖掘这些包装良好的库隐藏的细节,以便无限释放你的 TensorFlow.js 技能。

章节复习

我们通过几种常见的实践选项为 TensorFlow.js 设置了你的计算机。我们确保我们的机器已经准备好运行 TensorFlow.js,甚至下载并运行了一个打包好的模型来确定文本毒性。

章节挑战:卡车警报!

花点时间尝试一下MobileNet 模型,它可以查看图像并尝试对主要物件进行分类。这个模型可以传递任何 <img><video><canvas> 元素,并返回对该特定图形中所见内容的最有可能预测的数组。

MobileNet 模型已经经过训练,可以对1,000 种可能的物品进行分类,从石墙到垃圾车,甚至是埃及猫。人们已经使用这个库来检测各种有趣的事物。我曾经看到一些代码将网络摄像头连接到 MobileNet 来检测羊驼

对于这个章节挑战,你的任务是创建一个可以检测卡车的网站。给定一个输入图像,你要识别它是否是一辆卡车。当你从照片中检测到一辆卡车时,执行 alert("检测到卡车!")。默认情况下,MobileNet 包返回前三个检测结果。如果这三个中有任何一个在照片中看到卡车,你的警报应该像图 2-5 中一样通知用户。

带有活动警报的卡车检测器

图 2-5. 卡车检测器工作

你可以在附录 B 中找到这个挑战的答案。

复习问题

让我们回顾一下你在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. 常规 TensorFlow 能在浏览器中运行吗?

  2. TensorFlow.js 能访问 GPU 吗?

  3. 运行 TensorFlow.js 是否必须安装 CUDA?

  4. 如果我在 CDN 上没有指定版本,会发生什么?

  5. 毒性分类器如何识别违规行为?

  6. 我们何时会达到毒性的阈值?

  7. 毒性代码是否包含所有所需的文件?

  8. 我们是否需要进行任何张量工作来使用这个毒性库?

这些练习的解决方案可以在附录 A 中找到。

¹ TensorFlow 直到 2017 年 2 月 11 日才达到 1.0.0 版本。

² Node 比 Python 案例研究提高了 2 倍:https://oreil.ly/4Jrbu

³ 请注意,此版本已锁定在 1.2.2。

⁴ 毒性模型信息可在https://oreil.ly/Eejyi找到。

第三章:引入张量

“哇!”

—基努·里维斯(《比尔和特德的冒险》)

我们已经多次提到张量这个词,它是 TensorFlow.js 中的主要词汇,所以是时候了解这些结构是什么了。这一关键章节将让您亲身体验管理和加速数据的基本概念,这是教机器学习的核心。

我们将:

  • 解释张量的概念和术语

  • 创建、读取和销毁张量

  • 练习结构化数据的概念

  • 跨越使用张量来构建有用的东西的鸿沟

如果您对张量还不熟悉,请花些时间阅读本章。熟悉数据的这一方面将有助于您全面了解机器学习。

为什么要使用张量?

我们生活在一个充满数据的世界中,我们都知道数据最终都是由 1 和 0 组成的。对于我们许多人来说,这似乎是一种魔法。你用手机拍照,就会生成一些复杂的二进制文件。然后,你上下滑动,我们的二进制文件在瞬间从 JPG 变成 PNG。成千上万个未知的字节在微秒内生成和销毁,文件调整大小、重新格式化,对于你这些时髦的孩子,还有滤镜。你不能再被宠坏了。当你开始实际接触、感受和处理数据时,你必须告别无知的幸福。

引用 1998 年电影《刀锋》中的一句台词:

“你最好醒醒。你生活的世界只是一层糖衣。下面还有另一个世界。”

好吧,就像那样,但没有那么激烈。要训练一个人工智能,您需要确保您的数据是统一的,并且您需要理解和看到它。您不是在训练您的人工智能来统一解码 PNG 和 JPG 文件;您是在训练它对照片中实际内容的解码和模仿版本。

这意味着图像、音乐、统计数据以及您在 TensorFlow.js 模型中使用的任何其他内容都需要统一和优化的数据格式。理想情况下,我们的数据应该转换为数字容器,这些容器可以快速扩展,并直接与 GPU 或 Web Assembly 中的计算优化一起工作。您需要为我们的信息数据提供清晰简单的输入和输出。这些容器应该是无偏见的,可以容纳任何内容。欢迎来到张量的世界!

提示

即使是最熟练的 TensorFlow.js 专家,理解张量的用途和属性也是一个持续的练习。虽然本章作为一个出色的介绍,但如果您在使用张量方面遇到困难,也不必感到懈怠。随着您的进步,本章可以作为一个参考。

你好,张量

张量是一种结构化类型的数据集合。将一切转换为数字对于一个框架来说并不新鲜,但意识到最终数据如何形成取决于您可能是一个新概念。

正如第一章中提到的,所有数据都需要转化为数字,以便机器能够理解。张量是首选的信息格式,它们甚至为非数值类型提供了小的抽象。它们就像来自物理世界的电信号传输到我们人工智能大脑中一样。虽然没有规定数据应该如何结构化,但您需要保持一致以保持信号有序,这样我们的大脑就可以一遍又一遍地看到相同的模式。人们通常将他们的数据组织成组,比如数组和多维数组。

但是张量是什么?从数学上定义,张量只是任意维度的一组结构化数值。最终,这将解决为计算准备好的数据的优化分组。这意味着,从数学角度来看,传统的 JavaScript 数组是一个张量,2D 数组是一个张量,512D 数组也是一个张量。TensorFlow.js 张量是这些数学结构的具体体现,它们保存着加速信号,将数据输入和输出到机器学习模型中。

如果您熟悉 JavaScript 中的多维数组,您应该对张量的语法感到非常熟悉。当您向每个数组添加一个新维度时,通常会说您正在增加张量的

创建张量

无论您如何导入 TensorFlow.js,本书中的代码都假定您已经将库整合到一个名为 tf 的变量中,该变量将用于在所有示例中代表 TensorFlow.js。

注意

您可以阅读或从头开始编写代码,甚至在基于浏览器的 /tfjs 解决方案中运行这些基础示例,该解决方案可在书籍源代码中找到。为简单起见,我们将避免重复设置这些示例所需的 <script>import 标签,并简单地编写共享代码。

要创建您的第一个张量,我们将保持简单,您将使用一个一维 JavaScript 数组构建它(示例 3-1)。数组的语法和结构被应用到张量中。

示例 3-1。创建您的第一个张量
// creating our first tensor
const dataArray = [8, 6, 7, 5, 3, 0, 9]
const first = tf.tensor(dataArray) // ①

// does the same thing
const first_again = tf.tensor1d(dataArray) // ②

tf.tensor 如果传入一个一维数组,将创建一个一维张量。如果传入一个二维数组,将创建一个二维张量。

tf.tensor1d 如果传入一个一维数组,将创建一个一维张量。如果传入一个二维数组,将报错。

这段代码在内存中创建了一个包含七个数字的一维张量数据结构。现在这七个数字已经准备好进行操作、加速操作,或者仅仅作为输入。不过,我相信您已经注意到我们提供了两种执行相同操作的方式。

第二种方法提供了额外的运行时检查级别,因为您已经定义了期望的维度。确定所需的维度在您希望确保正在处理的数据的维度数量时非常有用。存在用于验证高达六个维度的方法,例如 tf.tensor6d

在本书中,我们将主要使用通用的 tf.tensor,但如果您发现自己深入进行复杂的项目,不要忘记您可以通过明确定义张量的期望维度来避免收到意外维度带来的困扰。

额外说明,虽然 示例 3-1 中的张量是一个自然数数组,但用于存储数字的默认数据类型是 Float32。浮点数(即带有小数点的数字,例如 2.71828)非常动态和令人印象深刻。它们通常可以处理您需要的大多数数字,并准备好接受之间的值。与 JavaScript 数组不同,张量的数据类型必须是同质的(全部相同类型)。这些类型只能是 Float32Int32、布尔型、complex64 或字符串,不能混合使用。

如果您希望强制创建的张量具有特定类型,请随时利用 tf.tensor 函数的第三个参数,该参数明确定义了张量的类型结构。

// creating a 'float32' tensor (the default)
const first = tf.tensor([1.1, 2.2, 3.3], null, 'float32') // ①

// an 'int32' tensor
const first_again = tf.tensor([1, 2, 3], null, 'int32') // ②

// inferred type for boolean
const the_truth = tf.tensor([true, false, false]) // ③

// Guess what this does
const guess = tf.tensor([true, false, false], null, 'int32') // ④

// What about this?
const guess_again = tf.tensor([1, 3.141592654, false]) // ⑤

这个张量被创建为 Float32 张量。在这种情况下,第三个参数是多余的。

生成的张量是 Int32 类型的,如果没有第三个参数,它将是一个 Float32 类型的。

生成的张量是一个布尔型张量。

生成的张量是一个 Int32 张量,布尔值被转换为 0 表示 false,1 表示 true。因此,变量 guess 包含数据 [1, 0, 0]

您可能认为这个疯狂的数组会报错,但输入值中的每个值都会转换为相应的 Float32,生成的张量数据为 [1, 3.1415927, 0]

您如何识别所创建的张量类型?就像任何 JavaScript 数组一样,张量配备了解释其属性的方法。有用的属性包括长度(size)、维度(rank)和数据类型(dtype)。

让我们应用我们所学到的知识:

const second = tf.tensor1d([8, 6, 7, 5, 3, 0, 9]) // ①

// Whoopsie!
try {
  const nope = tf.tensor1d([[1],[2]]) // ②
} catch (e) {
  console.log("That's a negative Ghost Rider")
}

console.log("Rank:", second.rank) // ③
console.log("Size:", second.size) // ④
console.log("Data Type:", second.dtype) // ⑤

这将创建一个成功的张量。您应该知道数据类型,维度和大小。

由于您正在使用tensor1d创建一个秩为二的张量,这将导致catch运行并记录一条消息。

简单数组的秩为一,因此它将打印1

大小是数组的长度,将打印7

从数字数组的张量数据类型将打印float32

祝贺您创建了您的第一批张量!可以肯定地说,掌握张量是驯服 TensorFlow.js 数据的核心。这些结构化的值桶是将数据输入和输出机器学习的基础。

数据练习的张量

假设您想制作一个 AI 来玩井字游戏(对于我在池塘对岸的朋友来说,这是零和叉)。与数据一样,现在是时候喝杯咖啡或茶,思考将真实世界数据转换为张量数据的正确方法了。

您可以存储游戏图像,教程字符串,或者只是游戏中的 X 和 O。图像和教程可能会令人印象深刻,但现在,让我们只考虑存储游戏板状态的想法。只有九个可能的方框可供玩耍,因此九个值的简单数组应该代表棋盘的任何给定状态。

值应该从左到右,从上到下读取吗?只要您保持一致,很少有关系。所有编码都是虚构的。但是,请记住张量解析为数字!这意味着虽然您可以存储字符串“X”和“O”,但它们最终还是会变成数字。让我们通过将它们映射到某种有意义的数字值来存储我们的 X 和 O。这是否意味着您只需将其中一个分配为 0,另一个分配为 42?我相信您可以找到一个适当反映游戏状态的策略。

让我们评估一个活动游戏的状态作为练习。花点时间回顾一下正在进行中的比赛的网格,如图 3-1 所示。如何将其转换为张量和数字?

TicTacToe Game

图 3-1。带有数据的游戏

也许这里显示的棋盘可以被读取并表示为一维张量。您可以从左到右,从上到下读取值。至于数字,让我们选择-1、0 和 1 来表示任何一个方格的三个可能值。表 3-1 显示了每个可能值的查找。

表 3-1。值到数字表

棋盘值 张量值
X 1
O -1
0

这将创建一个张量,如下所示:[1, 0, 0, 0, -1, 0, 1, 0, 0]。或者,它将创建一个 2D 张量,如下所示:[[1, 0, 0],[0, -1, 0],[1, 0, 0]]

既然您有了一个目标,让我们编写一些代码将棋盘转换为张量。我们甚至将探索张量创建的附加参数。

// This code creates a 1D `Float32` tensor
const a = tf.tensor([1, 0, 0, 0, -1, 0, 1, 0, 0])

// This code creates a 2D `Float32` tensor
const b = tf.tensor([[1, 0, 0],[0, -1, 0],[1, 0, 0]])

// This does the same as the above but with a 1D input
// array that is converted into a 2D `Float32` tensor
const c = tf.tensor([1, 0, 0, 0, -1, 0, 1, 0, 0], [3, 3]) // ①

// This code turns the 1D input array into a 2D Int32 tensor
const d = tf.tensor([1, 0, 0, 0, -1, 0, 1, 0, 0], [3, 3], 'int32') // ②

张量的第二个参数可以标识输入数据的期望形状。在这里,通过指定希望数据为 3 x 3 的秩二结构,将 1D 数组转换为 2D 张量。

张量的第三个参数标识您想要在推断的数据类型上使用的数据类型。由于您正在存储整数,因此可以指定类型int32。但是,默认的float32类型的范围非常大,可以轻松处理我们的数字。

当您创建用于表示数据的张量时,您可以决定如何格式化输入数据以及生成的张量结构应该是什么。随着您掌握机器学习的概念,您始终在磨练哪种数据效果最佳的直觉。

我们将在本书的后面回到这个井字棋问题。

巡回张量

随着本书的进展,我们将深入研究张量,因此现在是时候花点时间讨论它们为什么如此重要了。如果不了解我们正在利用的计算的规模,很难理解离开熟悉的 JavaScript 变量和引擎去使用老旧的数学的好处。

张量提供速度

现在你知道你可以制作张量并将数据表示为张量,那么进行这种转换有什么好处呢?我们已经提到,使用张量进行计算是由 TensorFlow.js 框架优化的。当你将 JavaScript 数字数组转换为张量时,你可以以极快的速度执行矩阵运算,但这到底意味着什么呢?

计算机在进行单个计算方面表现出色,并且进行大量计算有其好处。张量被设计用于大量并行计算。如果你曾经手动执行过矩阵和向量计算,你就会开始意识到加速计算的好处。

张量提供直接访问

即使没有机器学习,你仍然可以使用张量制作 3D 图形、内容推荐系统以及美丽的迭代函数系统(IFSs),比如图 3-2 中所示的谢尔宾斯基三角形。

谢尔宾斯基三角形

图 3-2. IFS 示例:谢尔宾斯基三角形

有很多关于图像、声音、3D 模型、视频等的库。它们都有一个共同点。尽管存在各种格式,但这些库会将数据以通用格式提供给你。张量就像那种原始、展开的数据格式,通过这种访问,你可以构建、读取或预测任何你想要的东西。

你甚至可以使用这些高级结构来修改图像数据(你将在第四章开始这样做)。在掌握了基础知识后,你将开始更多地享受张量函数。

张量批处理数据

在数据领域,你可能会发现自己在循环处理大量数据并担心文本编辑器崩溃。张量被优化用于高速批处理。本章末尾的小项目只有四个用户,以保持简单,但任何生产环境都需要准备好处理数十万的数据。

当你要求经过训练的模型在毫秒内执行类似人类操作的计算时,你将意识到张量的大部分好处。你将在第五章中早早看到这些例子。我们已经确定张量是令人印象深刻的结构,为 JavaScript 带来了大量加速和数学能力,因此你通常会在批处理中使用这种有益的结构。

内存中的张量

张量速度是有代价的。通常,当我们在 JavaScript 中完成一个变量时,当所有对该变量的引用完成时,内存会被干净地移除。这被称为自动垃圾检测和收集(AGDC),大多数 JavaScript 开发人员在不理解或关心这是如何工作的情况下就会发生。然而,你的张量并没有得到同样类型的自动关照。它们会在使用它们的变量被收集后继续存在。

释放张量

由于张量在垃圾回收中幸存,它们的行为与标准 JavaScript 不同,必须手动进行核算和释放。即使在 JavaScript 中一个变量被垃圾回收,与之关联的张量仍然会在内存中被孤立。你可以使用tf.memory()来访问当前计数和大小。这个函数返回一个报告活动张量的对象。

示例 3-2 中的代码展示了未收集的张量内存。

示例 3-2. 内存中遗留的张量
/* Check the number of tensors in memory
*  and the footprint size.
*  Both of these logs should be zero.
*/
console.log(tf.memory().numTensors)
console.log(tf.memory().numBytes)

// Now allocate a tensor
let speedy = tf.tensor([1,2,3])
// remove reference for JS
speedy = null

/* No matter how long we wait
*  this tensor is going to be there,
*  until you refresh the page/server.
*/
console.log(tf.memory().numTensors)
console.log(tf.memory().numBytes)

示例 3-2 中的代码将在日志中打印以下内容:

0
0
1
12

由于您已经知道张量用于处理大量加速数据,将这些庞大的块留在内存中是一个问题。通过一个小循环,您可能会泄漏整个计算机可用的 RAM 和 GPU。

幸运的是,所有张量和模型都有一个 .dispose() 方法,可以从内存中清除张量。当您在张量上调用 .dispose() 时,numTensors 将减少您刚刚释放的张量数量。

这意味着您必须以两种方式管理张量,产生四种可能的状态。表 3-2 显示了当 JavaScript 变量和 TensorFlow.js 张量被创建和销毁时发生的所有组合。

表 3-2. 张量状态

张量存活 张量已销毁
JavaScript 变量存活 此变量存活;您可以读取张量。 如果尝试使用此张量,将引发错误。
JavaScript 变量没有引用 这是一个内存泄漏。 这是一个正确销毁的张量。

简而言之,保持您的变量和张量处于活动状态以便访问它们,完成后处理张量并不要尝试访问它。

自动张量清理

幸运的是,张量确实有一个自动清理选项称为 tidy()。您可以使用 tidy 创建一个功能封装,它将清理所有未返回或标记为保留的张量。我们将在接下来的演示中帮助您理解 tidy,并在整本书中都会使用它。

您将很快习惯清理张量。确保学习以下代码,它将演示 tidy()keep() 的使用:

// Start at zero tensors
console.log('start', tf.memory().numTensors)

let keeper, chaser, seeker, beater
// Now we'll create tensors inside a tidy
tf.tidy(() => { // ①
  keeper = tf.tensor([1,2,3])
  chaser = tf.tensor([1,2,3])
  seeker = tf.tensor([1,2,3])
  beater = tf.tensor([1,2,3])
  // Now we're at four tensors in memory // ②
  console.log('inside tidy', tf.memory().numTensors)

  // protect a tensor
  tf.keep(keeper)
  // returned tensors survive
  return chaser
})

// Down to two // ③
console.log('after tidy', tf.memory().numTensors)

keeper.dispose() // ④
chaser.dispose() // ⑤

tidy 方法接受一个同步函数,并监视在此封闭中创建的张量。您不能在此处使用异步函数或承诺。如果您需要任何异步操作,您将不得不显式调用 .dispose

所有四个张量都已有效加载到内存中。

即使您没有显式调用 disposetidy 已经正确销毁了创建的两个张量(那两个没有被保留或返回的张量)。如果您现在尝试访问它们,将会收到错误信息。

显式销毁您在 tidy 中使用 tf.keep 保存的张量。

显式销毁您从 tidy 返回的张量。

如果所有这些都说得通,您已经学会了从内存中神奇地创建和移除张量的实践。

张量回家

值得注意的是,您甚至可以在适当的情况下混合张量和 JavaScript。示例 3-3 中的代码创建了一个张量的普通 JavaScript 数组。

示例 3-3. 混合 JS 和张量
const tensorArray = []
for (let i = 0; i < 10; i++) {
  tensorArray.push(tf.tensor([i, i, i]))
}

示例 3-3 的结果是一个包含 10 个张量的数组,值从[0,0,0][9,9,9]。与创建一个用于保存这些值的 2D 张量不同,您可以通过在数组中检索普通的 JavaScript 索引来轻松访问特定的张量。因此,如果您想要 [4,4,4],您可以使用 tensorArray[4] 获取它。然后,您可以使用简单的 tf.dispose(tensorArray) 从内存中销毁整个集合。

尘埃落定后,我们学会了如何创建和移除张量,但我们遗漏了一个关键部分,即张量将它们的数据返回给 JavaScript。张量非常适用于大型计算和速度,但 JavaScript 也有其优势。使用 JavaScript,您可以迭代,获取特定索引,或执行一系列 NPM 库计算,这在张量形式下要复杂得多。

在使用张量进行计算并获得好处之后,可以肯定地说,您始终需要将这些数据的结果返回到 JavaScript 中。

检索张量数据

如果您尝试将张量打印到控制台,您可以看到对象,但看不到底层数据值。要打印张量的数据,您可以调用张量的.print()方法,但这将直接将值发送到console.log而不是一个变量。查看张量的值对开发人员是有帮助的,但我们最终需要将这些值带入 JavaScript 变量中以便使用。

有两种方法可以检索张量。每种方法都有一个同步方法和一个异步方法。首先,如果您希望数据以相同的多维数组结构传递,您可以使用.array()获得异步结果,或者简单地使用.arraySync()获得同步值。其次,如果您希望保持极高精度的值,并将其展平为 1D 类型化数组,您可以使用同步的dataSync()和异步方法data()

以下代码探讨了使用先前描述的方法转换、打印和解析张量并进行张量操作的过程:

const snap = tf.tensor([1,2,3])
const crackle = tf.tensor([3.141592654])
const pop = tf.tensor([[1,2,3],[4,5,6]])

// this will show the structure but not the data
console.log(snap) // ①
// this will print the data but not the tensor structure
crackle.print() // ②

// Now let's go back to JavaScript
console.log('Welcome Back Array!', pop.arraySync()) // ③
console.log('Welcome Back Typed!', pop.dataSync()) // ④

// clean up our remaining tensors!
tf.dispose([snap, crackle, pop])

这个日志显示了保存张量及其相关属性的 JavaScript 结构。您可以看到形状,isDisposedInternal为 false,因为它尚未被处理,但这只是一个指向数据的指针,而不是包含数据。这个日志打印如下:

{
  "kept": false,
  "isDisposedInternal": false,
  "shape": [
    3
  ],
  "dtype": "float32",
  "size": 3,
  "strides": [],
  "dataId": {},
  "id": 4,
  "rankType": "1",
  "scopeId": 4
}

在张量上调用.print会直接将内部值的实际打印输出到控制台。这个日志打印如下:

Tensor
    [3.1415927]

.arraySync将 2D 张量的值作为 2D JavaScript 数组返回给我们。这个日志打印如下:

Welcome Back Array!
[
  [
    1,
    2,
    3
  ],
  [
    4,
    5,
    6
  ]
]

.dataSync给我们提供了 2D 张量的值作为一个 1D Float32Array对象,有效地将数据展平。记录一个类型化数组看起来像一个具有索引作为属性的对象。这个日志打印:

Welcome Back Typed!
{
  "0": 1,
  "1": 2,
  "2": 3,
  "3": 4,
  "4": 5,
  "5": 6
}

现在您知道如何管理张量了。您可以将任何 JavaScript 数据带入 TensorFlow.js 张量进行操作,然后在完成后将其清晰地带出来。

张量操作

现在是时候充分利用移动所有这些数据的价值了。您现在知道如何将大量数据移动到张量中,但让我们享受这个过程带来的好处。机器学习模型是由数学驱动的。任何依赖于线性代数的数学过程都将受益于张量。您也将受益,因为您不必编写任何复杂的数学运算。

张量和数学

假设您必须将一个数组的内容乘以另一个数组。在 JavaScript 中,您必须编写一些迭代代码。此外,如果您熟悉矩阵乘法,您会知道该代码并不像您最初想的那样简单。任何级别的开发人员都不应该为张量操作解决线性代数。

还记得如何正确地相乘矩阵吗?我也忘了。

91 82 13 15 23 62 25 66 63 X 1 23 83 33 12 5 7 23 61 = ?

将每个数字乘以相应位置并不像你们中的一些人可能想的那样简单;因为涉及到乘法和加法。计算左上角的值将是 91 x 1 + 82 x 33 + 13 x 7 = 2888。现在对新矩阵的每个索引重复八次这样的计算。计算这种简单乘法的 JavaScript 并不完全琐碎。

张量具有数学上的好处。我不必编写任何代码来执行以前的计算。虽然编写自定义代码不会很复杂,但会是非优化和冗余的。有用的、可扩展的数学运算是内置的。TensorFlow.js 使线性代数对于张量等结构变得易于访问和优化。我可以用以下代码快速得到以前矩阵的答案:

  const mat1 = [
    [91, 82, 13],
    [15, 23, 62],
    [25, 66, 63]
  ]

  const mat2 = [
    [1, 23, 83],
    [33, 12, 5],
    [7, 23, 61]
  ]

  tf.matMul(mat1, mat2).print()

在第二章中,毒性检测器下载了用于每个分类计算的大量数字。在毫秒内处理这些大量计算的行为是张量背后的力量。虽然我们将继续扩展张量计算的好处,但 TensorFlow.js 的整个原因是这样一个大量计算的复杂性是框架的领域,而不是程序员的领域。

推荐张量

凭借你迄今学到的技能,你可以构建一个简单的示例,展示 TensorFlow.js 如何处理真实场景的计算。以下示例被选择为张量的力量的一个例证,它欢迎精英和数学避免者。

注意

这一部分可能是你会接触到的最深的数学内容。如果你想深入了解支持机器学习的线性代数和微积分,我推荐一个由斯坦福大学提供、由吴恩达教授的免费在线课程

让我们用一些张量数据构建一些真实的东西。你将进行一系列简单的计算,以确定一些用户的偏好。这些系统通常被称为推荐引擎。你可能熟悉推荐引擎,因为它们建议你应该购买什么,下一部电影你应该看什么等等。这些算法是数字产品巨头如 YouTube、亚马逊和 Netflix 的核心。推荐引擎在任何销售任何东西的业务中都非常受欢迎,可能可以单独填写一本书。我们将实现一个简单的“基于内容”的推荐系统。发挥你的想象力,因为在生产系统中,这些张量要大得多。

在高层次上,你将做以下事情:

  1. 要求用户对乐队进行评分,从110

  2. 任何未知的乐队得到0

  3. 乐队和音乐风格将是我们的“特色”。

  4. 使用矩阵点积来确定每个用户喜欢的风格!

让我们开始创建一个推荐系统!这个小数据集将作为你所需的示例。正如你所注意到的,你在代码中混合了 JavaScript 数组和张量。将标签保留在 JavaScript 中,将计算推入张量是非常常见的。这不仅使张量专注于数字;还有国际化张量结果的好处。标签是这个操作中唯一依赖语言的部分。你会看到这个主题在本书中的几个示例和实际机器学习的真实世界中持续存在。

以下是数据:

const users = ['Gant', 'Todd',  'Jed', 'Justin'] // ①
const bands = [ // ②
  'Nirvana',
  'Nine Inch Nails',
  'Backstreet Boys',
  'N Sync',
  'Night Club',
  'Apashe',
  'STP'
]
const features = [ // ③
  'Grunge',
  'Rock',
  'Industrial',
  'Boy Band',
  'Dance',
  'Techno'
]

// User votes // ④
const user_votes = tf.tensor([
  [10, 9, 1, 1, 8, 7, 8],
  [6, 8, 2, 2, 0, 10, 0],
  [0, 2, 10, 9, 3, 7, 0],
  [7, 4, 2, 3, 6, 5, 5]
])

// Music Styles 5
const band_feats = tf.tensor([
  [1, 1, 0, 0, 0, 0],
  [1, 0, 1, 0, 0, 0],
  [0, 0, 0, 1, 1, 0],
  [0, 0, 0, 1, 0, 0],
  [0, 0, 1, 0, 0, 1],
  [0, 0, 1, 0, 0, 1],
  [1, 1, 0, 0, 0, 0]
])

这四个名称标签只是简单地存储在一个普通的 JavaScript 数组中。

你已经要求我们的用户对七支乐队进行评分。

一些简单的音乐流派可以用来描述我们的七支乐队,同样存储在一个 JavaScript 数组中。

这是我们的第一个张量,一个二级描述,每个用户的投票从110,其中“我不认识这支乐队”为0

这个张量也是一个二维张量,用于识别与每个给定乐队匹配的流派。每行索引代表了可以分类为真/假的流派的编码。

现在你已经拥有了张量中所需的所有数据。快速回顾一下,你可以看到信息的组织方式。通过阅读user_votes变量,你可以看到每个用户的投票。例如,你可以看到用户0,对应 Gant,给 Nirvana 评了10分,Apashe 评了7分,而 Jed 给了 Backstreet Boys10分。

band_feats变量将每个乐队映射到它们满足的流派。例如,索引1处的第二个乐队是 Nine Inch Nails,对 Grunge 和工业音乐风格有积极评分。为了简单起见,你使用了每种流派的二进制10,但在这里也可以使用一种标准化的数字比例。换句话说,[1, 1, 0, 0, 0, 0]代表了 Grunge 和 Rock 对于第 0 个乐队,也就是 Nirvana。

接下来,你将根据他们的投票计算每个用户最喜欢的流派:

// User's favorite styles
const user_feats = tf.matMul(user_votes, band_feats)
// Print the answers
user_feats.print()

现在user_feats包含用户在每个乐队的特征上的点积。我们打印的结果将如下所示:

Tensor
    [[27, 18, 24, 2 , 1 , 15],
     [14, 6 , 18, 4 , 2 , 10],
     [2 , 0 , 12, 20, 10, 10],
     [16, 12, 15, 5 , 2 , 11]]

这个张量显示了每个用户特征(在本例中是流派)的价值。用户0,对应 Gant,其最高价值在索引0处为27,这意味着他们在调查数据中最喜欢的流派是 Grunge。这些数据看起来相当不错。使用这个张量,你可以确定每个用户的喜好。

虽然数据以张量形式存在,但你可以使用一个叫做topk的方法来帮助我们识别每个用户的前k个值。要获取前k个张量或者仅仅通过识别它们的索引来确定前k个值的位置,你可以调用带有所需张量和大小的函数topk。在这个练习中,你将把k设置为完整特征集大小。

最后,让我们把这些数据带回 JavaScript。编写这段代码可以这样写:

// Let's make them pretty
const top_user_features = tf.topk(user_feats, features.length)
// Back to JavaScript
const top_genres = top_user_features.indices.arraySync() // ①
// print the results
users.map((u, i) => {
  const rankedCategories = top_genres[i].map(v => features[v]) // ②
  console.log(u, rankedCategories)
})

你将索引张量返回到一个二维 JavaScript 数组以获取结果。

你正在将索引映射回音乐流派。

结果日志如下所示:

Gant
[
  "Grunge",
  "Industrial",
  "Rock",
  "Techno",
  "Boy Band",
  "Dance"
]
Todd
[
  "Industrial",
  "Grunge",
  "Techno",
  "Rock",
  "Boy Band",
  "Dance"
]
Jed
[
  "Boy Band",
  "Industrial",
  "Dance",
  "Techno",
  "Grunge",
  "Rock"
]
Justin
[
  "Grunge",
  "Industrial",
  "Rock",
  "Techno",
  "Boy Band",
  "Dance"
]

在结果中,你可以看到 Todd 应该多听工业音乐,而 Jed 应该加强对男孩乐队的了解。两者都会对他们的推荐感到满意。

你刚刚做了什么?

你成功地将数据加载到张量中,这样做是有意义的,然后你对整个集合应用了数学计算,而不是对每个人进行迭代式的处理。一旦你得到了答案,你对整个集合进行了排序,并将数据带回 JavaScript 进行推荐!

你还能做更多吗?

你可以做很多事情。从这里开始,你甚至可以使用每个用户投票中的 0 来确定用户从未听过的乐队,并按照最喜欢的流派顺序推荐给他们!有一种非常酷的数学方法可以做到这一点,但这有点超出了我们第一个张量练习的范围。不过,恭喜你实现了在线销售中最受欢迎和流行的功能之一!

章节回顾

在本章中,你不仅仅是浅尝辄止地了解了张量。你深入了解了 TensorFlow.js 的基本结构,并掌握了根本。你正在掌握在 JavaScript 中应用机器学习的方法。张量是一个贯穿所有机器学习框架和基础知识的概念。

章节挑战:你有何特别之处?

现在你不再是一个张量新手,你可以像专业人士一样管理张量,让我们尝试一个小练习来巩固你的技能。在撰写本文时,JavaScript 没有内置的方法来清除数组中的重复项。虽然其他语言如 Ruby 已经有了uniq方法超过十年,JavaScript 开发人员要么手动编写解决方案,要么导入像 Lodash 这样的流行库。为了好玩,让我们使用 TensorFlow.js 来解决唯一值的问题。作为一个学到的教训的练习,思考一下这个问题:

给定这个美国电话号码数组,删除重复项。

// Clean up the duplicates
const callMeMaybe = tf.tensor([8367677, 4209111, 4209111, 8675309, 8367677])

确保您的答案是一个 JavaScript 数组。如果您在这个练习中遇到困难,可以查阅TensorFlow.js 在线文档。搜索关键术语的文档将指引您正确方向。

您可以在附录 B 中找到这个挑战的答案。

复习问题

让我们回顾一下您在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. 我们为什么要使用张量?

  2. 以下哪一个不是张量数据类型?

    1. Int32

    2. Float32

    3. 对象

    4. 布尔值

  3. 六维张量的秩是多少?

  4. 方法dataSync的返回数组的维数是多少?

  5. 当您将一个三维张量传递给tf.tensor1d时会发生什么?

  6. 在张量形状方面,ranksize之间有什么区别?

  7. 张量tf.tensor([1])的数据类型是什么?

  8. 张量的输入数组维度总是结果张量维度吗?

  9. 如何确定内存中张量的数量?

  10. tf.tidy能处理异步函数吗?

  11. 如何在tf.tidy内部创建的张量?

  12. 我可以用console.log看到张量的值吗?

  13. tf.topk方法是做什么的?

  14. 张量是为批量还是迭代计算进行优化的?

  15. 推荐引擎是什么?

这些练习的解决方案可以在附录 A 中找到。

第四章:图像张量

“但是那些不敢抓住荆棘的人

永远不应该渴望玫瑰。”

— 安妮·勃朗特

在上一章中,你创建并销毁了简单的张量。然而,我们的数据很小。正如你可能猜到的,打印张量只能带你走到这么远,而且在这么多维度上。你需要学会如何处理更常见的大张量。当然,在图像世界中这是真实的!这是一个令人兴奋的章节,因为你将开始处理真实数据,我们将能够立即看到你的张量操作的效果。

我们还将利用一些现有的最佳实践。正如你回忆的,在上一章中,你将一个井字棋游戏转换为张量。在这个简单的 3 x 3 网格的练习中,你确定了一种转换游戏状态的方法,但另一个人可能会想出完全不同的策略。我们需要确定一些常见的做法和行业诀窍,这样你就不必每次都重新发明轮子。

我们将:

  • 识别张量是什么使其成为图像张量

  • 手动构建一些图像

  • 使用填充方法创建大张量

  • 将现有图像转换为张量,然后再转换回来

  • 以有用的方式操作图像张量

当你完成本章时,你将能够自信地处理真实世界的图像数据,而这些知识很多都适用于一般张量的管理。

视觉张量

你可能会假设当图像转换为张量时,得到的张量将是二阶的。如果你忘记了二阶张量是什么样子,请查看第三章。很容易将一个 2D 图像想象成一个 2D 张量,只是像素颜色通常不能存储为单个数字。二阶张量仅适用于灰度图像。彩色像素的最常见做法是将其表示为三个独立的值。那些从小就接触颜料的人被教导使用红色、黄色和蓝色,但我们这些书呆子更喜欢红色、绿色、蓝色(RGB)系统。

注意

RGB 系统是艺术模仿生活的另一个例子。人眼使用 RGB,这是基于“加法”颜色系统——一种发射光的系统,就像计算机屏幕一样。你的美术老师可能用黄色覆盖绿色来帮助淡化随着添加更多而变暗的颜料的颜色,这是一种“减法”颜色系统,就像纸上的颜料一样。

一个像素通常是由红色、绿色和蓝色的有序量来着色,这些量在一个字节内。这个0-255值数组看起来像[255, 255, 255]对于整数,对于大多数寻求相同三个值的十六进制版本的网站来说,看起来像#FFFFFF。当我们的张量是数据类型int32时,这是使用的解释方法。当我们的张量是float32时,假定值在0-1范围内。因此,一个整数[255, 255, 255]代表纯白,但在浮点形式中等价的是[1, 1, 1]。这也意味着[1, 1, 1]float32张量中是纯白的,并且在int32张量中被解释为接近黑色。

根据张量数据类型的不同,从一个像素编码为[1, 1, 1],你会得到两种颜色极端,如图 4-1 所示。

颜色取决于张量类型

图 4-1。相同数据的显著颜色差异

这意味着要存储图像,你将需要一个三维张量。你需要将每个三值像素存储在给定的宽度和高度上。就像你在井字棋问题中看到的那样,你将不得不确定最佳的格式来做到这一点。在 TensorFlow 和 TensorFlow.js 中,将 RGB 值存储在张量的最后一个维度是一种常见做法。也习惯性地将值沿着高度、宽度,然后颜色维度进行存储。这对于图像来说可能看起来有点奇怪,但引用行然后列是矩阵的经典组织参考顺序。

警告

大多数人会按照宽度乘以高度来提及图像尺寸。一个 1024 x 768 的图像宽度为1024px,高度为768px,但正如我们刚刚所述,TensorFlow 图像张量首先存储高度,这可能有点令人困惑。同样的图像将是一个[768, 1024, 3]张量。这经常会让对视觉张量新手的开发人员感到困惑。

因此,如果你想要制作一个 4 x 3 的像素棋盘,你可以手动创建一个形状为[3, 4, 3]的 3D 数组。

代码将会是以下简单的形式:

const checky = tf.tensor([
  [
    [1, 1, 1],
    [0, 0, 0],
    [1, 1, 1],
    [0, 0, 0]
  ],
  [
    [0, 0, 0],
    [1, 1, 1],
    [0, 0, 0],
    [1, 1, 1]
  ],
  [
    [1, 1, 1],
    [0, 0, 0],
    [1, 1, 1],
    [0, 0, 0]
  ],
])

一个 4 x 3 像素的图像可能会很小,但如果我们放大几百倍,我们将能够看到我们刚刚创建的像素。生成的图像看起来会像图 4-2。

一个简单的 4 x 3 图像

图 4-2。4 x 3 的 TensorFlow.js 棋盘图像

你不仅限于 RGB,正如你可能期望的那样;在张量的 RGB 维度中添加第四个值将添加一个 alpha 通道。就像在 Web 颜色中一样,#FFFFFF00将是白色的零不透明度,具有红色、绿色、蓝色、alpha(RGBA)值为[1, 1, 1, 0]的张量像素也将是类似透明的。一个带有透明度的 1024 x 768 图像将存储在一个形状为[768, 1024, 4]的张量中。

作为前述两个系统的推论,如果最终通道只有一个值而不是三个或四个,生成的图像将是灰度的。

我们之前的黑白棋盘图案示例可以通过使用最后的知识大大简化。现在我们可以用张量构建相同的图像,代码如下:

const checkySmalls = tf.tensor([
  [[1],[0],[1],[0]],
  [[0],[1],[0],[1]],
  [[1],[0],[1],[0]]
])

是的,如果你简单地去掉那些内部括号并将其移动到一个简单的 2D 张量中,那也是可以的!

快速图像张量

我知道有一大群人在你的门口排队逐个像素地手绘图像,所以你可能会惊讶地发现有些人觉得写一些小的 1 和 0 很烦人。当然,你可以使用Array.prototype.fill创建数组,然后使用它来填充数组以创建可观的 3D 张量构造器,但值得注意的是,TensorFlow.js 已经内置了这个功能。

创建具有预填充值的大张量是一个常见的需求。实际上,如果你继续从第三章的推荐系统中工作,你将需要利用这些确切的功能。

现在,你可以使用tf.onestf.zerostf.fill方法手动创建大张量。tf.onestf.zeros都接受一个形状作为参数,然后构造该形状,每个值都等于10。因此,代码tf.zeros([768, 1024, 1])将创建一个 1024 x 768 的黑色图像。可选的第二个参数将是生成的张量的数据类型。

提示

通常,你可以通过使用tf.zeros创建一个空图像,通过模型预先分配内存。结果会立即被丢弃,后续调用会快得多。这通常被称为模型预热,当开发人员在等待网络摄像头或网络数据时寻找要分配的内容时,你可能会看到这种加速技巧。

正如你所想象的,tf.fill接受一个形状,然后第二个参数是用来填充该形状的值。你可能会想要将一个张量作为第二个参数传递,从而提高生成的张量的秩,但重要的是要注意这样做是行不通的。关于什么有效和无效的对比,请参见表 4-1。

表 4-1。填充参数:标量与向量

这有效 这无效
tf.fill([2, 2], 1) tf.fill([2, 2], [1, 1, 1])

你的第二个参数必须是一个单一值,用来填充你给定形状的张量。这个非张量值通常被称为标量。总之,代码tf.fill([200, 200, 4], 0.5)将创建一个 200 x 200 的灰色半透明正方形,如图 4-3 所示。

一个填充为 0.5 的图像

图 4-3. 带背景的 Alpha 通道图像张量

如果您对不能用优雅的颜色填充张量感到失望,那么我有一个惊喜给您!我们下一个创建大张量的方法不仅可以让您用张量填充,还可以让您用图案填充。

让我们回到您之前制作的 4 x 3 的方格图像。您手工编码了 12 个像素值。如果您想制作一个 200 x 200 的方格图像,那将是 40,000 个像素值用于简单的灰度。相反,我们将使用.tile方法来扩展一个简单的 2 x 2 张量。

// 2 x 2 checker pattern
  const lil = tf.tensor([  // ①
    [[1], [0]],
    [[0], [1]]
  ]);
  // tile it
  const big = lil.tile([100, 100, 1]) // ②

方格图案是一个二维的黑白张量。这可以是任何优雅的图案或颜色。

瓷砖大小为 100 x 100,因为重复的图案是 2 x 2,这导致了一个 200 x 200 的图像张量。

对于人眼来说,方格像素很难看清楚。不放大的情况下,方格图案可能看起来灰色。就像印刷点组成杂志的多种颜色一样,一旦放大,您就可以清楚地看到方格图案,就像在图 4-4 中一样。

使用瓷砖的结果

图 4-4. 10 倍放大的 200 x 200 方格张量

最后,如果所有这些方法对您的口味来说都太结构化,您可以释放混乱!虽然 JavaScript 没有内置方法来生成随机值数组,但 TensorFlow.js 有各种各样的方法可以精确地做到这一点。

简单起见,我最喜欢的是.randomUniform。这个张量方法接受一个形状,还可以选择一个最小值、最大值和数据类型。

如果您想构建一个 200 x 200 的灰度颜色的随机静态图像,您可以使用tf.randomUniform([200, 200, 1])或者tf.randomUniform([200, 200, 1], 0, 255, 'int32')。这两者将产生相同的(尽可能相同的)结果。

图 4-5 显示了一些示例输出。

200 x 200 随机

图 4-5. 200 x 200 随机值填充的张量

JPG、PNG 和 GIF,哦我的天啊!

好的,甘特!您已经谈论了一段时间的图像,但我们看不到它们;我们只看到张量。张量如何变成实际可见的图像?而对于机器学习来说,现有的图像如何变成张量?

正如您可能已经直觉到的那样,这将根据 JavaScript 运行的位置(特别是客户端和服务器)而有很大不同。要在浏览器上将图像解码为张量,然后再转换回来,您将受到浏览器内置功能的限制和赋予的力量。相反,在运行 Node.js 的服务器上的图像将不受限制,但缺乏易于的视觉反馈。

不要害怕!在本节中,您将涵盖这两个选项,这样您就可以自信地将 TensorFlow.js 应用于图像,无论媒介如何。

我们将详细审查以下常见情况:

  • 浏览器:张量到图像

  • 浏览器:图像到张量

  • Node.js:张量到图像

  • Node.js:图像到张量

浏览器:张量到图像

为了可视化、修改和保存图像,您将利用 HTML 元素和画布。让我们从给我们一种可视化我们学到的所有图形课程的方法开始。我们将在浏览器中将一个张量渲染到画布上。

首先,创建一个 400 x 400 的随机噪声张量,然后在浏览器中将张量转换为图像。为了实现这一点,您将使用tf.browser.toPixels。该方法将张量作为第一个参数,可选地为第二个参数提供一个画布以绘制。它返回一个在渲染完成时解析的 Promise。

注意

乍一看,将 canvas 作为可选参数是相当令人困惑的。值得注意的是,promise 将以Uint8ClampedArray的形式解析为张量作为参数,因此这是一个很好的方式来创建一个“准备好的 canvas”值,即使您没有特定的 canvas 在脑海中。随着OffscreenCanvas 的概念从实验模式转变为实际支持的 Web API,它可能会减少实用性。

要设置我们的第一个画布渲染,您需要在我们的 HTML 中有一个带有 ID 的画布,以便您可以引用它。对于那些熟悉 HTML 加载顺序复杂性的人来说,您需要在尝试从 JavaScript 中访问它之前使画布存在之前(或者遵循您网站的任何最佳实践,比如检查文档准备就绪状态):

<canvas id="randomness"></canvas>

现在您可以通过 ID 访问此画布,并将其传递给我们的browser.toPixels方法。

const bigMess = tf.randomUniform([400, 400, 3]); // ①
const myCanvas = document.getElementById("randomness"); // ②
tf.browser.toPixels(bigMess, myCanvas).then(() => { // ③
  // It's not bad practice to clean up and make sure we got everything
  bigMess.dispose();
  console.log("Make sure we cleaned up", tf.memory().numTensors);
});

创建一个 RGB 400 x 400 图像张量

在文档对象模型(DOM)中获取对我们画布的引用

使用我们的张量和画布调用browser.toPixels

如果此代码在异步函数中运行,您可以简单地等待browser.toPixels调用,然后清理。如果不使用 promise 或异步功能,dispose几乎肯定会赢得可能的竞争条件并导致错误。

浏览器:图像到张量

正如您可能已经猜到的,browser.toPixels有一个名为browser.fromPixels的对应方法。此方法将获取图像并将其转换为张量。对于我们来说,browser.fromPixels的输入非常动态。您可以传入各种元素,从 JavaScript ImageData 到 Image 对象,再到 HTML 元素如<img><canvas>,甚至<video>。这使得将任何图像编码为张量变得非常简单。

作为第二个参数,您甚至可以确定您想要的图像通道数(1、3、4),因此您可以优化您关心的数据。例如,如果您要识别手写,那么就没有真正需要 RGB。您可以立即从我们的张量转换中获得灰度张量!

要设置我们的图像到张量转换,您将探索两种最常见的输入。您将转换一个 DOM 元素,也将转换一个内存元素。内存元素将通过 URL 加载图像。

警告

如果到目前为止您一直在本地打开.html文件,那么这里将停止工作。您需要实际使用像 200 OK!这样的 Web 服务器或其他提到的托管解决方案来访问通过 URL 加载的图像。如果遇到困难,请参阅第二章。

要从 DOM 加载图像,您只需要在 DOM 上引用该项。在与本书相关的源代码中,我设置了一个示例来访问两个图像。跟随的最简单方法是阅读GitHub 上的第四章

让我们用一个简单的img标签和id设置我们的 DOM 图像:

<img id="gant" src="/gant.jpg" />

是的,那是我决定使用的一张奇怪的图片。我有可爱的狗,但它们很害羞,拒绝签署发布协议成为我书中的模特。作为一个爱狗人士可能会很“艰难”。现在您有了一张图片,让我们写一个简单的 JavaScript 来引用所需的图像元素。

提示

在尝试访问图像元素之前,请确保document已经加载完成。否则,您可能会收到类似“源宽度为 0”的神秘消息。这在没有 JavaScript 前端框架的实现中最常见。在没有任何东西等待 DOM 加载事件的情况下,我建议在尝试访问 DOM 之前订阅window的加载事件。

img放置并 DOM 加载完成后,您可以调用browser.fromPixels获取结果:

// Simply read from the DOM
const gantImage = document.getElementById('gant') // ①
const gantTensor = tf.browser.fromPixels(gantImage) // ②
console.log( // ③
  `Successful conversion from DOM to a ${gantTensor.shape} tensor`
)

获取对img标签的引用。

从图像创建张量。

记录证明我们现在有了一个张量!这将打印以下内容:

Successful conversion from DOM to a 372,500,3 tensor
警告

如果您遇到类似于 Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data. 的错误,这意味着您正在尝试从另一个服务器加载图像而不是本地。出于安全原因,浏览器会防止这种情况发生。查看下一个示例以加载外部图像。

完美!但是如果我们的图像不在页面的元素中怎么办?只要服务器允许跨域加载 (Access-Control-Allow-Origin "*"),您就可以动态加载和处理外部图像。这就是 JavaScript 图像对象示例 的用武之地。我们可以这样将图像转换为张量:

// Now load an image object in JavaScript
const cake = new Image() // ①
cake.crossOrigin = 'anonymous' // ②
cake.src = '/cake.jpg' // ③
cake.onload = () => { // ④
  const cakeTensor = tf.browser.fromPixels(cake) // ⑤
  console.log( // ⑥
    `Successful conversion from Image() to a ${cakeTensor.shape} tensor`
  )
}

创建一个新的 Image web API 对象。

这在这里是不必要的,因为文件在服务器上,但通常需要设置此选项以访问外部 URL。

给出图像的路径。

等待图像完全加载到对象中,然后再尝试将其转换为张量。

将图像转换为张量。

打印我们的张量形状以确保一切按计划进行。这将打印以下内容:从 Image() 成功转换为 578,500,3 张量

通过结合两种先前的方法,您可以创建一个单页面,其中显示一个图像元素并将两个张量的值打印到控制台(参见 图 4-6)。

工作代码的截图

图 4-6. 两个图像变成张量的控制台日志

通过图像的日志,您可以看到它们都是 500 像素宽的 RGB 图像。如果修改第二个参数,您可以轻松地将这些图像中的任何一个转换为灰度或 RGBA。您将在本章后面修改我们的图像张量。

Node:张量到图像

在 Node.js 中,没有用于渲染的画布,只有安静高效地写文件。您将使用 tfjs-node 保存一个随机的 400 x 400 RGB。虽然图像张量是逐像素的值,但典型的图像格式要小得多。JPG 和 PNG 具有各种压缩技术、头部、特性等。生成的文件内部看起来与我们漂亮的 3D 图像张量完全不同。

一旦张量转换为它们的编码文件格式,您将使用 Node.js 文件系统库 (fs) 将文件写出。现在您已经有了一个计划,让我们探索保存张量到 JPG 和 PNG 的功能和设置。

编写 JPG

要将张量编码为 JPG,您将使用一个名为 node.encodeJpeg 的方法。此方法接受图像的 Int32 表示和一些选项,并返回一个包含结果数据的 promise。

您可能注意到的第一个问题是,输入张量 必须 是具有值 0-255 的 Int32 编码,而浏览器可以处理浮点和整数值。也许这是一个优秀的开源贡献者的绝佳机会!?

提示

任何具有值 0-1Float32 张量都可以通过将其乘以 255 然后转换为 int32 的代码来转换为新的张量,例如:myTensor.mul(255).asType('int32')

从张量中写入 JPG,就像在*GitHub 的第四章节中的 chapter4/node/node-encode中发现的那样,可以简单地这样做:

  const bigMess = tf.randomUniform([400, 400, 3], 0, 255); // ①
  tf.node.encodeJpeg(bigMess).then((f) => { // ②
    fs.writeFileSync("simple.jpg", f); // ③
    console.log("Basic JPG 'simple.jpg' written");
  });

创建一个 400 x 400 的图像张量,其中包含随机的 RGB 像素。

使用张量输入调用 node.encodeJpeg

生成的数据将使用文件系统库写入。

因为您要写入的文件是 JPG,您可以启用各种配置选项。让我们再写入另一张图片,并在此过程中修改默认设置:

const bigMess = tf.randomUniform([400, 400, 3], 0, 255);
tf.node
  .encodeJpeg(
    bigMess,
    "rgb", // ①
    90,    // ②
    true,  // ③
    true,  // ④
    true,  // ⑤
    "cm",  // ⑥
    250,   // ⑦
    250,   // ⑧
    "Generated by TFJS Node!" // ⑨
  )
  .then((f) => {
    fs.writeFileSync("advanced.jpg", f);
    console.log("Full featured JPG 'advanced.jpg' written");
  });

format:您可以使用grayscalergb覆盖默认的颜色通道,而不是匹配输入张量。

quality:调整 JPG 的质量。较低的数字会降低质量,通常是为了减小文件大小。

progressive:JPG 具有从上到下加载或逐渐清晰的渐进加载能力。将其设置为 true 可以启用渐进加载格式。

optimizeSize:花费一些额外的周期来优化图像大小,而不会修改质量。

chromaDownsampling:这是一个技巧,其中照明比颜色更重要。它修改了数据的原始分布,使其对人眼更清晰。

densityUnit:选择每英寸或每厘米的像素;一些奇怪的人反对公制系统。

xDensity:设置 x 轴上的像素密度单位。

yDensity:设置 y 轴上的像素密度单位。

xmpMetadata:这是一个非可见的消息,存储在图像元数据中。通常,这是为许可和寻宝活动保留的。

根据您写入 JPG 的原因,您可以充分配置或忽略这些选项来自 Node.js!图 4-7 显示了您刚刚创建的两个 JPG 文件的文件大小差异。

两个 JPG 文件大小

图 4-7. 我们两个示例的文件大小

写入 PNG

写入 PNG 的功能明显比 JPG 有限得多。正如您可能猜到的那样,我们将有一个友好的方法来帮助我们,它被称为node.encodePng。就像我们的朋友 JPG 一样,该方法期望我们的张量的整数表示,值范围在0-255之间。

我们可以轻松地写入 PNG 如下:

const bigMess = tf.randomUniform([400, 400, 3], 0, 255);
tf.node.encodePng(bigMess).then((f) => {
  fs.writeFileSync("simple.png", f);
  console.log("Basic PNG 'simple.png' written");
});

PNG 参数并不那么先进。您只有一个新参数,而且它是一个神秘的参数!node.encodePng的第二个参数是一个压缩设置。该值可以在-19之间任意取值。默认值为1,表示轻微压缩,而9表示最大压缩。

提示

您可能认为-1表示无压缩,但通过实验,0表示无压缩。实际上,-1激活了最大压缩。因此,-1 和 9 实际上是相同的。

由于 PNG 在压缩随机性方面表现糟糕,您可以将第二个参数设置为9,得到与默认设置大小相近的文件:

tf.node.encodePng(bigMess, 9).then((f) => {
  fs.writeFileSync("advanced.png", f);
  console.log("Full featured PNG 'advanced.png' written");
});

如果您想看到实际的文件大小差异,请尝试打印一些易于压缩的内容,比如tf.zeros。无论如何,您现在可以轻松地从张量生成 PNG 文件。

注意

如果您的张量使用了 alpha 通道,您不能使用 JPG 等格式;您将不得不保存为 PNG 以保留这些数据。

Node:图像到张量

Node.js 是一个出色的工具,用于训练机器学习模型,因为它具有直接的文件访问和解码图像的速度。在 Node.js 上将图像解码为张量与编码过程非常相似。

Node 提供了解码 BMP、JPG、PNG 甚至 GIF 文件格式的功能。但是,正如您可能期望的那样,还有一个通用的node.decodeImage方法,能够自动进行简单的识别查找和转换。您现在将使用decodeImage,并留下decodeBMP等待您需要时查看。

对于图像的最简单解码是直接将文件传递给命令。为此,您可以使用标准的 Node.js 库fspath

这个示例代码依赖于一个名为cake.jpg的文件进行加载和解码为张量。此演示中使用的代码和图像资源可在 GitHub 的第四章chapter4/node/node-decode中找到。

import * as tf from '@tensorflow/tfjs-node'
import * as fs from 'fs'
import * as path from 'path'

const FILE_PATH = 'files'
const cakeImagePath = path.join(FILE_PATH, 'cake.jpg')
const cakeImage = fs.readFileSync(cakeImagePath) // ①

tf.tidy(() => {
  const cakeTensor = tf.node.decodeImage(cakeImage) // ②
  console.log(`Success: local file to a ${cakeTensor.shape} tensor`)

  const cakeBWTensor = tf.node.decodeImage(cakeImage, 1) // ③
  console.log(`Success: local file to a ${cakeBWTensor.shape} tensor`)
})

您使用文件系统库将指定的文件加载到内存中。

您将图像解码为与导入图像的颜色通道数量相匹配的张量。

您将此图像解码为灰度张量。

正如我们之前提到的,解码过程还允许解码 GIF 文件。一个明显的问题是,“GIF 的哪一帧?”为此,您可以选择所有帧或动画 GIF 的第一帧。node.decodeImage方法有一个标志,允许您确定您的偏好。

注意

物理学家经常争论第四维是时间还是不是时间。不管关于 4D 闵可夫斯基时空是否是现实的争论,对于动画 GIF 来说,这是一个已被证明的现实!为了表示动画 GIF,您使用一个四阶张量。

这个示例代码解码了一个动画 GIF。您将要使用的示例 GIF 是一个 500 x 372 的动画 GIF,有 20 帧:

const gantCakeTensor = tf.node.decodeImage(gantCake, 3, 'int32', true)
console.log(`Success: local file to a ${gantCakeTensor.shape} tensor`)

对于node.decodeImage参数,您提供图像数据,接着是三个颜色通道,作为一个int32结果张量,最后一个参数是true

传递true让方法知道展开动画 GIF 并返回一个 4D 张量,而false会将其剪裁为 3D。

我们的结果张量形状,正如您可能期望的那样,是[20, 372, 500, 3]

常见的图像修改

将图像导入张量进行训练是强大的,但很少是直接的。当图像用于机器学习时,它们通常有一些常见的修改。

常见的修改包括:

  • 被镜像以进行数据增强

  • 调整大小以符合预期的输入大小

  • 裁剪出脸部或其他所需部分

您将在机器学习中执行许多这些操作,并且您将在接下来的两章中看到这些技能被使用。第十二章的毕业项目将大量依赖这项技能。让我们花点时间来实现一些这些日常操作,以完善您对图像张量的舒适度。

镜像图像张量

如果您正在尝试训练一个识别猫的模型,您可以通过镜像您现有的猫照片来使数据集翻倍。微调训练图像以增加数据集是一种常见做法。

要为图像翻转张量数据,您有两个选项。一种是以一种方式修改图像张量的数据,使图像沿宽度轴翻转。另一种方法是使用tf.image.flipLeftRight,这通常用于图像批次。让我们两者都做一下。

要翻转单个图像,您可以使用tf.reverse并指定您只想翻转包含图像宽度像素的轴。正如您已经知道的,这是图像的第二个轴,因此您将传递的索引是1

在本章的相应源代码中,您显示一幅图像,然后在旁边的画布上镜像该图像。您可以在 GitHub 的simple/simple-image-manipulation/mirror.html中访问此示例。此操作的完整代码如下:

// Simple Tensor Flip
const lemonadeImage = document.getElementById("lemonade");
const lemonadeCanvas = document.getElementById("lemonadeCanvas");
const lemonadeTensor = tf.browser.fromPixels(lemonadeImage);
const flippedLemonadeTensor = tf.reverse(lemonadeTensor, 1) // ①
tf.browser.toPixels(flippedLemonadeTensor, lemonadeCanvas).then(() => {
  lemonadeTensor.dispose();
  flippedLemonadeTensor.dispose();
})

reverse 函数将轴索引1翻转以反转图像。

因为您了解底层数据,将此转换应用于您的图像是微不足道的。您可以尝试沿高度或甚至 RGB 轴翻转。任何数据都可以被反转。

Figure 4-8 显示了在轴1上使用tf.reverse的结果。

翻转单个轴

图 4-8。tf.reverse 用于轴设置为 1 的 lemonadeTensor
提示

反转和其他数据操作方法并不局限于图像。您可以使用这些方法来增强非视觉数据集,如井字棋和类似的游戏。

我们还应该回顾另一种镜像图像的方法,因为这种方法可以处理一组图像的镜像,并且在处理图像数据时暴露了一些非常重要的概念。毕竟,我们的目标是尽可能依赖张量的优化,并尽量远离 JavaScript 的迭代循环。

第二种镜像图像的方法是使用tf.image.flipLeftRight。这种方法旨在处理一组图像,并且一组 3D 张量基本上是 4D 张量。对于我们的演示,您将取一张图像并将其制作成一组一张的批次。

要扩展单个 3D 图像的维度,您可以使用tf.expandDims,然后当您想要反转它(丢弃不必要的括号)时,您可以使用tf.squeeze。这样,您可以将 3D 图像移动到 4D 以进行批处理,然后再次缩小。对于单个图像来说,这似乎有点愚蠢,但这是一个很好的练习,可以帮助您理解批处理和张量维度变化的概念。

因此,一个 200 x 200 的 RGB 图像起始为[200, 200, 3],然后您扩展它,实质上使其成为一个堆叠。结果形状变为[1, 200, 200, 3]

您可以使用以下代码在单个图像上执行tf.image.flipLeftRight

// Batch Tensor Flip
const cakeImage = document.getElementById("cake");
const cakeCanvas = document.getElementById("cakeCanvas");
const flipCake = tf.tidy(() => {
  const cakeTensor = tf.expandDims( // ①
    tf
      .browser.fromPixels(cakeImage) // ②
      .asType("float32") // ③
  );
  return tf
    .squeeze(tf.image.flipLeftRight(cakeTensor)) // ④
    .asType("int32"); // ⑤
})
tf.browser.toPixels(flipCake, cakeCanvas).then(() => {
  flipCake.dispose();
});

张量的维度被扩展。

将 3D 图像导入为张量。

在撰写本节时,image.flipLeftRight期望图像是一个float32张量。这可能会在未来发生变化。

翻转图像批次,然后在完成后将其压缩回 3D 张量。

image.flipLeftRight返回0-255的值,因此您需要确保发送给browser.toPixels的张量是int32,这样它才能正确渲染。

这比我们使用tf.reverse更复杂一些,但每种策略都有其自身的优点和缺点。在可能的情况下,充分利用张量的速度和巨大计算能力是至关重要的。

调整图像张量的大小

许多 AI 模型期望特定的输入图像尺寸。这意味着当您的用户上传 700 x 900 像素的图像时,模型正在寻找一个尺寸为 256 x 256 的张量。调整图像大小是处理图像输入的核心。

注意

调整图像张量的大小以用于输入是大多数模型的常见做法。这意味着任何与期望输入严重不成比例的图像,如全景照片,当调整大小以用于输入时可能表现糟糕。

TensorFlow.js 有两种优秀的方法用于调整图像大小,并且两者都支持图像批处理:image.resizeNearestNeighborimage.resizeBilinear。我建议您在进行任何视觉调整时使用image.resizeBilinear,并将image.resizeNearestNeighbor保留用于当图像的特定像素值不能被破坏或插值时。速度上有一点小差异,image.resizeNearestNeighborimage.resizeBilinear快大约 10 倍,但差异仍然以每次调整的毫秒数来衡量。

直白地说,resizeBilinear会模糊,而resizeNearestNeighbor会像素化,当它们需要为新数据进行外推时。让我们使用这两种方法放大图像并进行比较。您可以在simple/simple-image-manipulation/resize.html中查看此示例。

// Simple Tensor Flip
const newSize = [768, 560] // 4x larger // ①
const littleGantImage = document.getElementById("littleGant");
const nnCanvas = document.getElementById("nnCanvas");
const blCanvas = document.getElementById("blCanvas");
const gantTensor = tf.browser.fromPixels(littleGantImage);

const nnResizeTensor = tf.image.resizeNearestNeighbor( // ②
  gantTensor,
  newSize,
  true // ③
)
tf.browser.toPixels(nnResizeTensor, nnCanvas).then(() => {
  nnResizeTensor.dispose();
})

const blResizeTensor = tf.image.resizeBilinear( // ④
  gantTensor,
  newSize,
  true // ⑤
)
const blResizeTensorInt = blResizeTensor.asType('int32') // ⑥
tf.browser.toPixels(blResizeTensorInt, blCanvas).then(() => {
  blResizeTensor.dispose();
  blResizeTensorInt.dispose();
})

// All done with ya
gantTensor.dispose();

将图像大小增加 4 倍,以便您可以看到这两者之间的差异。

使用最近邻算法调整大小。

第三个参数是alignCorners;请始终将其设置为 true。¹

使用双线性算法调整大小。

始终将此设置为true(参见3)。

截至目前,resizeBilinear返回一个float32,你需要进行转换。

如果你仔细观察图 4-9 中的结果,你会看到最近邻的像素呈现锐利的像素化,而双线性的呈现柔和的模糊效果。

调整大小方法

图 4-9. 使用调整大小方法的表情符号(有关图像许可证,请参见附录 C)
警告

使用最近邻算法调整大小可能会被恶意操纵。如果有人知道你最终的图像尺寸,他们可以构建一个看起来只在那个调整大小时不同的邪恶图像。这被称为对抗性预处理。更多信息请参见https://scaling-attacks.net

如果你想看到鲜明对比,你应该尝试使用两种方法调整本章开头创建的 4 x 3 图像的大小。你能猜到哪种方法会在新尺寸上创建一个棋盘格,哪种方法不会吗?

裁剪图像张量

在我们最后一轮的基本图像张量任务中,我们将裁剪一幅图像。我想指出,就像我们之前的镜像练习一样,有一种适用于批量裁剪大量图像的版本,称为image.cropAndResize。知道这种方法的存在,你可以利用它来收集和规范化图像的部分用于训练,例如,抓取照片中检测到的所有人脸并将它们调整到相同的输入尺寸以供模型使用。

目前,你只需从 3D 张量中裁剪出一些张量数据的简单示例。如果你想象这在空间中,就像从一个更大的矩形蛋糕中切出一个小矩形薄片。

通过给定切片的起始位置和大小,你可以在任何轴上裁剪出你想要的任何部分。你可以在 GitHub 上的simple/simple-image-manipulation/crop.html找到这个例子。要裁剪单个图像,请使用以下代码:

// Simple Tensor Crop
const startingPoint = [0, 40, 0]; // ①
const newSize = [265, 245, 3]; // ②
const lemonadeImage = document.getElementById("lemonade");
const lemonadeCanvas = document.getElementById("lemonadeCanvas");
const lemonadeTensor = tf.browser.fromPixels(lemonadeImage);

const cropped = tf.slice(lemonadeTensor, startingPoint, newSize) // ③
tf.browser.toPixels(cropped, lemonadeCanvas).then(() => {
  cropped.dispose();
})
lemonadeTensor.dispose();

从下方0像素开始,向右40像素,并且在红色通道上。

获取接下来的265像素高度,245像素宽度,以及所有三个 RGB 值。

将所有内容传入tf.slice方法。

结果是原始图像的精确裁剪,你可以在图 4-10 中看到。

使用切片裁剪张量

图 4-10. 使用tf.slice裁剪单个图像张量

新的图像工具

你刚刚学会了三种最重要的图像操作方法,但这并不意味着你的能力有所限制。新的 AI 模型将需要新的图像张量功能,因此,TensorFlow.js 和辅助库不断添加用于处理和处理图像的方法。现在,你可以更加自如地在单个和批量形式中利用和依赖这些工具。

章节回顾

从可编辑张量中编码和解码图像使你能够进行逐像素的操作,这是很少有人能做到的。当然,你已经学会了为了我们在 AI/ML 中的目标而学习视觉张量,但事实上,如果你愿意,你可以尝试各种疯狂的图像操作想法。如果你愿意,你可以做以下任何一种:

  • 铺设一个你自己设计的像素图案

  • 从另一幅图像中减去一幅图像以进行艺术设计

  • 通过操纵像素值在图像中隐藏一条消息

  • 编写分形代码或其他数学可视化

  • 去除背景图像颜色,就像绿幕一样

在本章中,你掌握了创建、加载、渲染、修改和保存大型结构化数据张量的能力。处理图像张量不仅简单,而且非常有益。你已经准备好迎接任何挑战。

章节挑战:排序混乱

使用您在本章和之前章节学到的方法,您可以用张量做一些非常令人兴奋和有趣的事情。虽然这个挑战没有我能想到的特定实用性,但它是对您所学内容的有趣探索。作为对所学课程的练习,请思考以下问题:

如何生成一个随机的 400 x 400 灰度张量,然后沿一个轴对随机像素进行排序?

如果您完成了这个挑战,生成的张量图像将会像图 4-11 那样。

一个随机噪声张量排序

图 4-11. 沿宽度轴排序的 400 x 400 随机性

您可以使用本书中学到的方法来解决这个问题。如果遇到困难,请查阅TensorFlow.js 在线文档。在文档中搜索关键词将指引您正确方向。

您可以在附录 B 中找到这个挑战的答案。

复习问题

让我们回顾一下您在本章编写的代码中学到的知识。请花点时间回答以下问题:

  1. 如果一个图像张量包含值0-255,为了正确渲染它需要什么类型的数据?

  2. 一个 2 x 2 的红色Float32在张量形式中会是什么样子?

  3. tf.fill([100, 50, 1], 0.2)会创建什么样的图像张量?

  4. 真或假:要保存一个 RGBA 图像,您必须使用一个四阶图像张量。

  5. 真或假:randomUniform如果给定相同的输入,将会创建相同的输出。

  6. 在浏览器中将图像转换为张量应该使用什么方法?

  7. 在 Node.js 中对 PNG 进行编码时,第二个参数应该使用什么数字以获得最大压缩?

  8. 如果您想要将图像张量上下翻转,您该如何做?

  9. 哪个更快?

    1. 循环遍历一组图像并调整它们的大小

    2. 将一组图像作为四阶张量进行批处理并调整整个张量的大小

  10. 以下结果的秩和大小是多少:

    [.keep-together]#`tf.slice(myTensor, [0,0,0], [20, 20, 3])`?#
    

这些练习的解决方案可以在附录 A 中找到。

¹ TensorFlow 对alignCorners的实现存在错误,可能会有问题

第五章:介绍模型

“他从哪里弄来那些美妙的玩具?”

—杰克·尼科尔森(蝙蝠侠

现在您已经进入大联盟。在第二章中,您访问了一个完全训练好的模型,但您根本不需要了解张量。在这里的第五章,您将能够利用您的张量技能直接与您的模型一起工作,没有训练轮。

最后,您将开始利用大多数机器学习的大脑。模型可能看起来像黑匣子。通常,它们期望特定的张量形状输入,并输出特定的张量形状。例如,假设您已经训练了一个狗或猫分类器。输入可能是一个 32 x 32 的 3D RGB 张量,输出可能是一个从零到一的单个张量值,表示预测。即使您不了解这种设备的内部工作原理,至少使用具有定义结构的模型应该是简单的。

我们将:

  • 利用训练好的模型来预测各种答案

  • 识别我们现有张量操作技能的好处

  • 了解谷歌的 TFHub.dev 托管

  • 了解对象定位

  • 学习如何叠加边界框以识别图像的某些方面

本章将教您直接访问模型。您不会依赖于可爱的包装库来照顾。如果愿意,您甚至可以围绕现有的 TensorFlow.js 模型编写自己的包装库。掌握了本章的技能,您可以开始将突破性的机器学习模型应用于任何网站。

加载模型

我们知道我们需要将模型加载到内存中,最好是加载到像张量这样的 GPU 加速内存中,但是从哪里加载?作为一种祝福和诅咒,答案是“任何地方!”在软件中加载文件是很常见的,因此在 TensorFlow.js 中有各种答案。

为了加剧这个问题,TensorFlow.js 支持两种不同的模型格式。幸运的是,这些选项的组合并不复杂。您只需要知道需要哪种类型的模型以及从哪里访问它。

目前,TensorFlow.js 中有两种模型类型,每种类型都有其自己的优缺点。最简单且最可扩展的模型称为层模型。这种模型格式允许您检查、修改甚至拆解模型以进行调整。该格式非常适合重新调整和调整。另一种模型格式是图模型。图模型通常更加优化和计算效率更高。使用图模型的成本是模型更加“黑匣子”,由于其优化,更难以检查或修改。

模型类型很简单。如果要加载层模型,您需要使用方法loadLayersModel,如果要加载 GraphDef 模型,则需要使用方法loadGraphModel。这两种模型类型各有利弊,但这超出了本章的范围。关键是加载所需模型类型几乎没有复杂性;只是一个问题是哪种类型,然后使用相应的方法。最重要的方面是第一个参数,即模型数据的位置。

提示

在本书结束时,您将对层模型和图模型类型之间的关键差异有相当扎实的理解。每次引入一个模型时,请注意使用了哪种模型。

本节解释了模型位置的多样性选项以及将它们绑定在一起的简单统一 URI 语法。

通过公共 URL 加载模型

使用公共 URL 加载模型是在 TensorFlow.js 中访问模型的最常见方法。正如您在第二章中记得的那样,当您加载毒性检测模型时,您从公共网络下载了文件的几个片段,每个片段大小为 4 MB,可以缓存。模型知道要下载文件的位置。这是通过单个 URL 到单个文件完成的。最初请求的模型文件是一个简单的 JavaScript 对象表示(JSON)文件,随后的文件是从该 JSON 文件中识别出的神经网络的权重。

从 URL 加载 TensorFlow.js 模型需要主动托管相邻的模型文件(相同的相对文件夹)。这意味着一旦您为模型的 JSON 文件提供路径,它通常会引用同一目录级别的连续文件中的权重。期望的结构如下:

Site
├─── Example Folder
├─── index.html
├─── Model Folder
│   ├─── model.json
│   └─── group1-shard1of3
│   └─── group1-shard2of3
│   └─── group1-shard3of3
...

移动或拒绝访问这些额外文件将导致您的模型无法使用并出现错误。根据服务器环境的安全性和配置,这可能是一个难点。因此,您应始终验证每个文件是否具有适当的 URL 访问权限。

注意

到目前为止,我们已经介绍了三种运行 TensorFlow.js 的主要方法。它们是简单的 200 OK!托管、使用 Parcel 打包的 NPM 和使用 Node.js 托管的服务器。在我们告诉您如何为这些情况正确加载模型之前,您能否确定哪种方法会出现问题?

200 OK!Chrome 的 Web 服务器示例不会出现问题,因为文件夹中的所有内容都是无优化或安全性地托管的。Parcel 为我们提供了一些功能,如转换、错误日志记录、HMR 和捆绑。有了这些功能,我们的 JSON 和权重文件不会被传递到分发文件夹,也就是dist文件夹,除非进行一些调整。

在 Parcel.js 2.0 中(在撰写本文时尚未正式发布),您将有更多选项用于静态文件,但目前,有一个简单的解决方案适用于我们将使用的 Parcel 1.x。您可以安装一个名为parcel-plugin-static-files-copy的插件,以允许本地静态托管模型文件。本书相关存储库中使用的代码利用了这个插件。

该插件通过有效地使放置在static目录中的任何文件从根 URL 公开访问。例如,放置在static/model中的model.json文件将可以作为localhost:1234/model/model.json访问。

无论您使用哪种 Web 解决方案,都需要验证模型文件的安全性和捆绑是否适合您。对于未受保护的公共文件夹,只需将所有文件上传到像 Amazon Web Services(AWS)和 Simple Storage Service(S3)这样的服务即可。您需要使整个存储桶公开,或者每个相邻文件都必须明确公开。验证您可以访问 JSON BIN 文件是很重要的。缺少或受限制的模型片段的错误消息令人困惑。您会看到一个404,但错误会继续到第二个更加难以理解的错误,就像图 5-1 中所示的那样。

缺少 bin 文件的错误截图。

图 5-1. 错误:JSON 可用但没有 bin 文件
提示

Create React App 是一个用于简单 React 网站的流行工具。如果您使用 Create React App,public文件夹中的文件将默认从根 URL 访问。将public视为我们 Parcel 解决方案的static文件夹。两者都非常有效,并已经为模型托管进行了测试。

从其他位置加载模型

模型不必位于公共 URL 中。TensorFlow 有方法允许您从本地浏览器存储IndexedDB 存储以及在 Node.js 中,本地文件系统访问模型文件。

其中一个重要的好处是,您可以从公共 URL 加载的模型在本地缓存,以便您的应用程序可以脱机准备。其他原因包括速度、安全性,或者仅仅是因为您可以。

浏览器文件

本地浏览器存储和 IndexedDB 存储是两种用于保存指定页面的文件的 Web API。与存储小数据片段(如单个变量)的 cookie 不同,Window.localStorage和 IndexedDB API 是客户端存储,能够处理文件等其他重要结构化数据跨浏览器会话。

公共 URL 具有httphttps方案;但是,这些方法在 URI 中使用不同的方案。要从本地存储加载模型,您将使用localstorage://model-name URI,要从 IndexedDB 加载模型,您将使用indexeddb://model-name URI。

除了提供的方法外,您可以存储和检索 TensorFlow.js 模型的位置没有限制。归根结底,您只需要数据,因此您可以使用任何自定义的IOHandler加载模型。例如,甚至已经有将模型完全转换为 JSON 文件的概念验证工作,权重已编码,因此您可以根据需要从任何位置调用require,甚至通过捆绑器。

文件系统文件

要访问文件系统中的文件,您需要使用一个具有权限获取所需文件的 Node.js 服务器。浏览器被沙箱化,目前无法使用此功能。

幸运的是,这与以前的 API 类似。使用file:方案来标识给定文件的路径,就像这样:file://path/to/model.json。就像在浏览器示例中一样,辅助文件必须位于同一文件夹中并且可访问。

我们第一个使用的模型

现在您熟悉了将模型加载到内存的机制,您可以在项目中使用模型。当您在第二章中使用毒性模型时,这对您进行了自动化,但是现在,您熟悉了张量和模型访问,可以处理一个模型,而无需所有保护包代码。

您需要一个简单的模型用于第一个示例。正如您所记得的,您在第三章中将井字棋棋盘编码为练习。让我们从您现有知识的基础上构建,不仅编码一个井字棋比赛,还将该信息传递到训练模型进行分析。训练模型将预测并返回最佳下一步的答案。

本节的目标是询问 AI 模型推荐哪些移动,这些移动在图 5-2 中有所说明。

三个示例游戏状态

图 5-2。三个游戏状态

这些游戏中的每一个处于不同的情况:

情景 A

这是空白的,允许 AI 进行第一步。

情景 B

现在轮到 O 走棋了,我们期望 AI 通过在右上角的方格中下棋来阻止潜在的失败。

情景 C

现在轮到 X 走棋了,我们期望 AI 在顶部中间移动并取得胜利!

让我们看看 AI 推荐什么,通过对这三种状态进行编码并打印模型的输出。

加载、编码和询问模型

您将使用简单的 URL 来加载模型。这个模型将是一个 Layers 模型。这意味着您将使用tf.loadLayersModel和路径到本地托管模型文件来加载。在本例中,模型文件将托管在model/ttt_model.json

注意

本示例的训练井字棋模型可以在本书的相关GitHub中访问。JSON 文件大小为 2 KB,权重文件(ttt_model.weights.bin)大小为 22 KB。对于一个井字棋求解器来说,这 24 KB 的负载并不算太大!

为了转录游戏棋盘状态,编码会有一点差异。你需要告诉 AI 它是为哪个团队在玩。你还需要一个可以对 X 和 O 无动于衷的 AI。因为情景 B 是在询问 AI 关于 O 而不是 X 的建议,我们需要一个灵活的编码系统。不要让 X 总是代表 1,将 AI 分配为 1,对手分配为-1。这样我们可以让 AI 处于玩 X 或 O 的情况。表 5-1 显示了查找每种可能值的情况。

表 5-1. 网格到数字的转换

棋盘值 张量值
AI 1
对手 -1
0

所有三个游戏需要被编码,然后堆叠成一个单一的张量传递给 AI 模型。然后模型提供三个答案,每种情况一个。

这是完整的过程:

  1. 加载模型。

  2. 编码三个单独的游戏状态。

  3. 将状态堆叠成一个单一的张量。

  4. 要求模型打印结果。

将输入堆叠到模型是一种常见的做法,可以让你的模型处理加速内存中的任意数量的预测。

堆叠增加了结果的维度。在 1D 张量上执行这个操作会创建一个 2D 张量,依此类推。在这种情况下,你有三个用 1D 张量表示的棋盘状态,所以堆叠它们将创建一个[3, 9]的二阶张量。大多数模型支持对它们的输入进行堆叠或批处理,输出将类似地堆叠,并与输入索引匹配的答案。

这段代码可以在 GitHub 仓库的chapter5/simple/simple-ttt-model找到,看起来是这样的:

tf.ready().then(() => { // ①
  const modelPath = "model/ttt_model.json" // ②
  tf.tidy(() => {
    tf.loadLayersModel(modelPath).then(model => { // ③
      // Three board states
      const emptyBoard = tf.zeros([9]) // ④
      const betterBlockMe = tf.tensor([-1, 0, 0, 1, 1, -1, 0, 0, -1]) // ⑤
      const goForTheKill = tf.tensor([1, 0, 1, 0, -1, -1, -1, 0, 1]) // ⑥

      // Stack states into a shape [3, 9]
      const matches = tf.stack([emptyBoard, betterBlockMe, goForTheKill]) // ⑦
      const result = model.predict(matches) // ⑧
      // Log the results
      result.reshape([3, 3, 3]).print() // ⑨
    })
  })
})

使用tf.ready,当 TensorFlow.js 准备好时解析。不需要 DOM 访问。

虽然模型是两个文件,但只需要识别 JSON 文件。它了解并加载任何额外的模型文件。

loadLayersModel模型解析为完全加载的模型。

一个空棋盘是九个零,代表情景 A。

编码为 X 等于-1代表情景 B。

编码为 X 等于1代表情景 C。

使用tf.stack将三个 1D 张量组合成一个 2D 张量。

使用.predict来要求模型识别最佳的下一步。

原始输出将被形状化为[3, 9],但这是一个很好的情况,通过重新塑造输出使其更易读。打印结果在三个 3 x 3 的网格中,这样我们可以像游戏棋盘一样阅读它们。

警告

当使用loadLayersModel甚至loadGraphModel时,TensorFlow.js 库依赖于fetch web API 的存在。如果在 Node.js 中使用这种方法,你需要使用像node-fetch这样的包来填充fetch

前述代码成功地将三场比赛转换为 AI 模型期望的张量格式,并通过模型的predict()方法运行这些值进行分析。结果将打印到控制台,并看起来像我们在图 5-3 中看到的样子。

井字棋模型的结果

图 5-3. 我们代码生成的结果为[3, 3, 3]形状的张量

这个神奇的方法是模型的predict()函数。该函数让模型知道为给定的输入生成输出预测。

解释结果

对于一些人来说,这个结果张量完全有意义,对于其他人,你可能需要一点上下文。结果再次是下一步最佳移动的概率。最高的数字获胜。

为了得到一个正确的概率,答案需要相加得到 100%,而它们确实相加得到了。让我们看看在这里显示的空井字棋板结果在情景 1 中:

[
  [0.2287459, 0.0000143, 0.2659601],
  [0.0000982, 0.0041204, 0.0001773],
  [0.2301052, 0.0000206, 0.270758 ]
],

如果你像我这样傻乎乎地把这九个值输入到你的计算器(TI-84 Plus CE 永远是我的最爱!),它们会相加得到数字 1。这意味着每个对应的值都是该位置的百分比投票。我们可以看到四个角都有一个显著(接近 25%)的结果。这是有道理的,因为在井字棋中,从一个角开始是最好的策略,其次是中间,它有次高的价值。

因为底部右侧有 27%的投票,这将是 AI 最有可能的移动。让我们看看 AI 在另一个情景中的表现。如果你还记得,在图 5-2 的情景 B 中,AI 需要移动到右上角来阻止。AI 的结果张量在情景 2 中显示:

[
  [0.0011957, 0.0032045, 0.9908957],
  [0.000263 , 0.0006491, 0.0000799],
  [0.0010194, 0.0002893, 0.0024035],
],

顶部右侧的值为 99%,所以模型正确地阻止了给定的威胁。机器学习模型的一个有趣之处是其他移动仍然有值,包括已经被占据的空格。

最后一个情景是一个编码的张量,用来查看模型是否能够获胜井字棋。预测批次的结果在情景 3 中显示:

[
  [0.0000056, 0.9867876, 0.0000028],
  [0.0003809, 0.0001524, 0.0011258],
  [0.0000328, 0.0114983, 0.0000139]
],

结果是 99%(四舍五入)确定顶部中间是最佳移动,这是正确的。其他移动甚至都不接近。所有三个预测结果似乎不仅是有效的移动,而且是给定状态下的正确移动。

你已经成功地加载并与一个模型进行交互,让它提供结果。凭借你刚刚获得的技能,你可以编写自己的井字棋游戏应用。我想互联网上对井字棋游戏的需求不会很大,但如果提供了相同结构的训练模型,你可以使用 AI 制作各种游戏!

提示

大多数模型都会有一些相关的文档,帮助你识别正确的输入和输出,但 Layers 模型有一些属性,如果需要帮助,你可以访问这些属性。期望的输入形状可以在model.input.shape中看到,输出可以在model.outputShape中看到。这些属性在 Graph 模型上不存在。

清理棋盘后

在这个例子中,TensorFlow.js 模型被包装在一个tidy中,并且在代码完成后会自动释放内存。在大多数情况下,你不会这么快完成你的模型。重要的是要注意,你必须像处理张量一样调用.dispose()来处理模型。模型被加速处理方式相同,因此它们有相同的清理成本。

重新加载网页通常会清除张量,但长时间运行的 Node.js 服务器将不得不监视和验证张量和模型是否被处理。

我们的第一个 TensorFlow Hub 模型

现在你已经正确地编码、加载和处理了少量数据通过一个自定义模型,你应该花一点时间挑战自己。在这一部分,你将加载一个规模更大的模型从 TensorFlow Hub,并处理一张图片。井字棋是九个值的输入,而大多数图片是包含数千个值的张量。

你将要加载的模型是目前最大和最令人印象深刻的模型之一,Inception v3。Inception 模型是一个令人印象深刻的网络,最初在 2015 年创建。这第三个版本已经训练了数十万张图片。这个模型有 91.02 MB,可以对 1,001 种不同的对象进行分类。来自第二章的 Chapter Challenge 中的 MobileNet-wrapped NPM 包很棒,但不像你即将使用的模型那样强大。

探索 TFHub

Google 已经开始免费托管像 Inception v3 这样的模型在其自己的 CDN 上。对于这种大型模型大小的情况,拥有一个可靠且令人印象深刻的版本化 CDN 对于像我们经常为 JavaScript 做的模型非常有用。您可以在 https://tfhub.dev 上访问数百个经过训练并准备就绪的 TensorFlow 和 TensorFlow.js 模型。TensorFlow.js 有一种特殊的方式来识别您的模型是否托管在 TFHub 上;我们只需在确定了模型 URL 后,在配置中添加 { fromTFHub: true }

当您浏览 TFHub 时,您可以看到各种发布者和每个模型的解释。这些解释很关键,因为正如我们已经确定的那样,模型对于输入和输出的期望是非常具体的。您可以在 与 Inception v3 相关的 TFHub 页面 上了解更多信息。这个模型是由 Google 构建的,提供的版本经过了广泛的训练。如果您渴望获取更多信息,不妨浏览一下 关于该模型的发表论文

在 TFHub 页面上,您可以获得使用模型的这两个关键见解。首先,预期的输入图片尺寸应为 299 x 299,值应为 0-1,并且应该像我们在之前的井字棋示例中一样进行批处理。其次,模型返回的结果是一个具有 1,001 个值的单维张量,最大的值最有可能(类似于井字棋返回的九个值)。这可能听起来有点混乱,但该页面使用了一些基于统计的术语来表达这一点:

输出是一批 logits 向量。Logits 中的索引是原始训练中分类的 num_classes = 1001 个类。

返回一个数值结果是有用的,但是像往常一样,我们需要将其映射回一个有用的标签。在井字棋中,我们将索引映射到棋盘上的位置,而在这种情况下,我们将值的索引映射到相应的标签,这些标签遵循相同的索引。TFHub 页面 分享了一个 TXT 文件,其中包含了所有必要标签的正确顺序,您将使用这些标签创建一个数组来解释预测结果。

连接 Inception v3

现在您知道 Inception v3 模型可以对照片进行分类,并且您已经了解了输入和输出规范。这就像是井字棋问题的一个更大版本。然而,会有新的障碍。例如,打印 1,001 个数字并不会提供有用的信息。您需要使用 topk 将巨大的张量解析回一个有用的上下文中。

以下代码可在 GitHub 仓库的 chapter5/simple/simple-tfhub 文件夹中找到。该代码依赖于一个具有 id mystery 的神秘图片。理想情况下,AI 可以为我们解决这个谜题:

tf.ready().then(() => {
  const modelPath =
    "https://tfhub.dev/google/tfjs-model/imagenet/inception_v3/classification/3/default/1"; // ①
  tf.tidy(() => {
    tf.loadGraphModel(modelPath, { fromTFHub: true }).then((model) => { // ②
      const mysteryImage = document.getElementById("mystery");
      const myTensor = tf.browser.fromPixels(mysteryImage);
      // Inception v3 expects an image resized to 299x299
      const readyfied = tf.image
        .resizeBilinear(myTensor, [299, 299], true) // ③
        .div(255) // ④
        .reshape([1, 299, 299, 3]); // ⑤

      const result = model.predict(readyfied); // ⑥
      result.print();  // ⑦

      const { values, indices } = tf.topk(result, 3); // ⑧
      indices.print(); // ⑨

      // Let's hear those winners
      const winners = indices.dataSync();
      console.log(` 10
         First place ${INCEPTION_CLASSES[winners[0]]},
         Second place ${INCEPTION_CLASSES[winners[1]]},
         Third place ${INCEPTION_CLASSES[winners[2]]}
      `);
    });
  });
});

这是 Inception 模型的 TFHub 的 URL。

加载图模型并将 fromTFHub 设置为 true。

图片被调整为 299 x 299。

fromPixels 的结果转换为介于 0 和 1 之间的值(对数据进行归一化)。

将 3D 张量转换为单批次 4D 张量,就像模型期望的那样。

对图片进行预测。

打印内容太多被截断了。

恢复前三个值作为我们的猜测。

打印前三个预测索引。

将索引映射到它们的标签并打印出来。INCEPTION_CLASSES 是一个标签数组,映射到模型输出。

在本章的相关代码中,您会发现三幅图片,您可以将其设置为本节中的神秘图片。Inception v3 令人印象深刻地正确识别了所有三幅图片。查看 图 5-4 中捕获的结果。

Inception 正确识别磁带播放器

图 5-4. Inception v3 图像的分类结果

从照片中可以看出,Inception 的第一个选择是“磁带播放器”,我认为这非常准确。其次,它看到了一个“磁带播放器”,老实说我不知道这和“磁带播放器”有什么不同,但我不是超级模型。最后,第三高的值是“收音机”,这就是我会说的。

你通常不需要像这样的大型模型,但随着新模型被添加到 TFHub,你知道你有选择。偶尔浏览现有模型。你会看到很多关于图像分类的模型。对图像进行分类是 AI 入门中比较令人印象深刻的任务之一,但为什么要止步于此呢?

我们的第一个叠加模型

到目前为止,你一直在处理简单的输出模型。井字棋识别你的下一步,Inception 对照片进行分类,为了全面,你将在电影中展示 AI 的经典视觉效果,即在照片中识别物体的边界框。AI 不是对整个照片进行分类,而是在照片中突出显示特定的边界框,就像图 5-5 中那样。

气球图像的边界框

图 5-5. 边界框叠加

通常,模型的边界框输出相当复杂,因为它处理各种类别和重叠框。通常,模型会让你使用一些数学方法来正确清理结果。与其处理这些,不如专注于在 TensorFlow.js 中绘制预测输出中的单个矩形。有时这被称为对象定位

这个最终练习的模型将是一个宠物脸部检测器。该模型将尽力为我们提供一个边界坐标集,指示它认为宠物脸部位于何处。通常不难说服人们看可爱的狗和猫,但这个模型可能有各种应用。一旦你有了宠物脸部的位置,你可以使用这些数据来训练额外的模型,比如识别宠物或检查它们可爱的鼻子是否需要 boop。你懂的...科学!

定位模型

这个模型是在一个名为Oxford-IIIT 宠物数据集上训练的。这个小巧的、大约 2MB 的模型期望一个 256 x 256 的Float32输入 RGB 宠物图像,并输出四个数字来识别围绕宠物脸部的边界框。1D 张量中的四个数字是左上角点和右下角点。

这些点表示为 0 到 1 之间的值,作为图像的百分比。你可以使用模型结果信息定义一个矩形,如图 5-6 所示。

4 个值如何变成两个点的显示

图 5-6. 四个值变成两个点

代码的开头将与之前的代码类似。你将首先将图像转换为张量,然后通过模型运行。以下代码可以在 GitHub 仓库的chapter5/simple/simple-object-localization中找到。

const petImage = document.getElementById("pet");
const myTensor = tf.browser.fromPixels(petImage);
// Model expects 256x256 0-1 value 3D tensor
const readyfied = tf.image
  .resizeNearestNeighbor(myTensor, [256, 256], true)
  .div(255)
  .reshape([1, 256, 256, 3]);

const result = model.predict(readyfied);
// Model returns top left and bottom right
result.print();

标记检测

现在你可以将结果坐标绘制为图像上的矩形。在 TensorFlow.js 中绘制检测是一个常见的任务。在图像上绘制张量结果的基本方法需要你将图像放在一个容器中,然后在图像上方放置一个绝对位置的画布。现在当你在画布上绘制时,你将在图像上绘制。¹ 从侧面看,布局将类似于图 5-7。

DOM 的 3D 视图

图 5-7. 画布的堆叠视图

对于这节课,CSS 已经直接嵌入到 HTML 中以方便。图像和画布布局如下:

<div style="position: relative; height: 80vh"> <!-- ① -->
  <img id="pet" src="/dog1.jpg" height="100%" />
  <canvas
    id="detection"
    style="position: absolute; left: 0;"
  ><canvas/> <!-- ② -->
</div>

包含的div是相对定位的,并且锁定在页面高度的 80%处。

画布以绝对位置放置在图像上。

对于简单的矩形,您可以使用画布上下文的strokeRect方法。strokeRect方法不像模型返回的那样需要两个点。它需要一个起点,然后是宽度和高度。要将模型点转换为宽度和高度,您只需减去每个顶点以获得距离。图 5-8 显示了这种计算的可视化表示。

计算宽度和高度的解释

图 5-8。宽度和高度是 X 和 Y 之间的差异计算

使用起点、覆盖矩形的宽度和高度,您可以用几行代码在画布上按比例绘制它。记住,张量输出是一个百分比,需要在每个维度上进行缩放。

// Draw box on canvas
const detection = document.getElementById("detection");
const imgWidth = petImage.width;
const imgHeight = petImage.height;
detection.width = imgWidth; // ①
detection.height = imgHeight;
const box = result.dataSync(); // ②
const startX = box[0] * imgWidth; // ③
const startY = box[1] * imgHeight;
const width = (box[2] - box[0]) * imgWidth; // ④
const height = (box[3] - box[1]) * imgHeight;
const ctx = detection.getContext("2d");
ctx.strokeStyle = "#0F0";
ctx.lineWidth = 4;
ctx.strokeRect(startX, startY, width, height); // ⑤

使检测画布与其所覆盖的图像大小相同。

获取边界框结果。

将起点 X 和 Y 缩放回图像。

通过从 X[1]减去 X[2]来找到框的宽度,然后通过图像宽度进行缩放。Y[1]和 Y[2]也是如此。

现在使用画布的 2D 上下文来绘制所需的矩形。

结果是在给定点处完美放置的边界框。自己看看图 5-9。

识别宠物脸部位置

图 5-9。宠物脸部定位

在运行此项目时的一个误解可能是您经历的检测和绘制很慢。这是错误的。很明显,当页面加载时,边界框出现之前会有延迟;然而,您正在经历的延迟包括加载模型并将其加载到某种加速内存中(有时称为模型预热)。尽管这有点超出了本章的目标,但如果您调用model.predict并再次绘制,您会在微秒内看到结果。您在本节中创建的画布+TensorFlow.js 结构可以轻松支持桌面计算机上每秒 60 帧以上。

具有大量边界框和标签的模型使用类似的strokeRect调用来勾画识别对象的位置。有各种各样的模型,它们各自识别图像的各个方面。在 TensorFlow.js 世界中,修改画布以在图像上绘制信息的实践非常有用。

章节回顾

了解模型的输入和输出是关键。在本章中,您最终看到了数据的全部过程。您转换了输入,将其传递给经过训练的模型,并解释了结果。模型可以接受各种各样的输入并提供同样广泛的输出。现在,无论模型需要什么,您都有一些令人印象深刻的经验可以借鉴。

章节挑战:可爱的脸

想象一下,我们的宠物脸部定位是更大过程中的第一步。假设您正在识别宠物脸部,然后将宠物脸部传递给另一个模型,该模型将寻找舌头以查看宠物是否发热和喘气。通常会像这样在管道中组织多个模型,每个模型都调整到自己特定的目的。

根据上一段代码中宠物脸部的位置,编写额外的代码来提取宠物的脸并为需要 96 x 96 图像输入的模型做准备。您的答案将是一个批量裁剪,如图 5-10。

只有狗的脸

图 5-10。目标[1, 96, 96, 3]张量,只包含脸部

尽管这个练习是为了裁剪宠物的脸以供第二个模型使用,但它也可以很容易地成为一个“宠物匿名化器”,需要您模糊宠物的脸。浏览器中的人工智能应用是无限的。

你可以在附录 B 中找到这个挑战的答案。

复习问题

让我们回顾一下你在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. 在 TensorFlow.js 中可以加载哪些类型的模型?

  2. 你需要知道一个模型被分成了多少个碎片吗?

  3. 除了公共 URL 之外,还有哪些地方可以加载模型?

  4. loadLayersModel 返回什么?

  5. 如何清除已加载模型的内存?

  6. Inception v3 模型的预期输入形状是什么?

  7. 使用哪个画布上下文方法可以绘制一个空矩形?

  8. 从 TFHub 加载模型时,你必须向加载方法传递哪个参数?

这些问题的解决方案可以在附录 A 中找到。

¹ 你不一定要使用画布;如果你愿意,你可以移动一个 DOM 对象,但是画布提供了简单和复杂的动画,速度很快。

第六章:高级模型和 UI

“做到之前总是看似不可能。”

—纳尔逊·曼德拉

您已经有了理解模型的基线。您已经消化和利用了模型,甚至在叠加中显示了结果。看起来可能无限制。但是,您已经看到模型往往以各种复杂的方式返回信息。对于井字棋模型,您只想要一个移动,但它仍然返回所有九个可能的框,留下了一些清理工作,然后您才能利用模型的输出。随着模型变得更加复杂,这个问题可能会加剧。在本章中,我们将选择一个广泛和复杂的模型类型进行对象检测,并通过 UI 和概念来全面了解可能遇到的任务。

让我们回顾一下您当前的工作流程。首先,您选择一个模型。确定它是一个 Layers 模型还是 Graph 模型。即使您没有这些信息,您也可以通过尝试以某种方式加载它来弄清楚。

接下来,您需要确定模型的输入和输出,不仅是形状,还有数据实际代表的内容。您批处理数据,对模型调用predict,然后输出就可以了,对吗?

不幸的是,您还应该知道一些其他内容。一些最新和最伟大的模型与您所期望的有显著差异。在许多方面,它们要优越得多,但在其他方面,它们更加繁琐。不要担心,因为您已经在上一章中建立了张量和画布叠加的坚实基础。通过一点点指导,您可以处理这个新的高级模型世界。

我们将:

  • 深入了解理论如何挑战您的张量技能

  • 了解高级模型特性

  • 学习许多新的图像和机器学习术语

  • 确定绘制多个框以进行对象检测的最佳方法

  • 学习如何在画布上为检测绘制标签

当您完成本章时,您将对实现高级 TensorFlow.js 模型的理论要求有深刻的理解。本章作为对您今天可以使用的最强大模型之一的认知演练,伴随着大量的学习。这不会很难,但请做好学习准备,并不要回避复杂性。如果您遵循本章中解释的逻辑,您将对机器学习的核心理论和实践有深刻的理解和掌握。

再谈 MobileNet

当您在TFHub.dev上浏览时,您可能已经看到我们的老朋友 MobileNet 以许多不同的风格和版本被提及。一个版本有一个简单的名字,ssd_mobilenet_v2,用于图像对象检测(请参见图 6-1 中的突出显示部分)。

多么令人兴奋!看起来您可以从之前的 TensorFlow Hub 示例中获取代码,并将模型更改为查看一组边界框及其相关类,对吗?

MobileNet SSD on TFHub

图 6-1. 用于对象检测的 MobileNet

这样做后,您立即收到一个失败消息,要求您使用model.executeAsync而不是model.predict(请参见图 6-2)。

MobileNet Predict Error

图 6-2. 预测不起作用

那么出了什么问题?到目前为止,您可能有一大堆问题。

  • 模型想要的executeAsync是什么?

  • 为什么这个 MobileNet 模型用于对象检测?

  • 为什么这个模型规范不关心输入大小?

  • “SSD”在机器学习中的名称中代表什么?

警告

在 Parcel 中,您可能会收到关于regeneratorRuntime未定义的错误。这是由于 Babel polyfill 中的弃用。如果您遇到此错误,您可以添加core-jsregenerator-runtime包并在主文件中导入它们。如果您遇到此问题,请参见本章的相关GitHub 代码

这是一个需要更多信息、理论和历史来理解的高级模型的完美例子。现在也是学习一些我们为方便起见而保留的概念的好时机。通过本章的学习,您将准备好处理一些新术语、最佳实践和复杂模型的特性。

SSD MobileNet

到目前为止,书中已经提到了两种模型的名称,但没有详细说明。MobileNet 和 Inception 是由谷歌 AI 团队创建的已发布的模型架构。在下一章中,您将设计自己的模型,但可以肯定地说,它们不会像这两个知名模型那样先进。每个模型都有一组特定的优点和缺点。准确性并不总是模型的唯一度量标准。

MobileNet 是一种用于低延迟、低功耗模型的特定架构。这使得它非常适合设备和网络。尽管基于 Inception 的模型更准确,但 MobileNet 的速度和尺寸使其成为边缘设备上分类和对象检测的标准工具。

查看由谷歌发布的性能和延迟图表,比较设备上的模型版本。您可以看到,尽管 Inception v2 的大小是 MobileNetV2 的几倍,需要更多计算才能进行单个预测,但 MobileNetV2 速度更快,虽然准确性不如 Inception,但仍然接近。MobileNetV3 甚至有望在尺寸略微增加的情况下更准确。这些模型的核心研究和进展使它们成为经过良好测试的资源,具有已知的权衡。正是因为这些原因,您会看到相同的模型架构在新问题中反复使用。

前面提到的这两种架构都是由谷歌用数百万张图片进行训练的。MobileNet 和 Inception 可以识别的经典 1,001 类来自一个名为ImageNet的知名数据集。因此,在云中的许多计算机上进行长时间训练后,这些模型被调整为立即使用。虽然这些模型是分类模型,但它们也可以被重新用于检测对象。

就像建筑物一样,模型可以稍作修改以处理不同的目标。例如,一个剧院可以从最初用于举办现场表演的目的进行修改,以便支持 3D 特色电影。是的,需要进行一些小的更改,但整体架构是可以重复使用的。对于从分类到对象检测重新用途的模型也是如此。

有几种不同的方法可以进行对象检测。一种方法称为基于区域的卷积神经网络(R-CNN)。不要将 R-CNN 与 RNN 混淆,它们是完全不同的,是机器学习中的真实事物。基于区域的卷积神经网络听起来可能像《哈利波特》中的咒语,但实际上只是通过查看图像的补丁来检测对象的一种流行方法,使用滑动窗口(即重复采样图像的较小部分,直到覆盖整个图像)。R-CNN 通常速度较慢,但非常准确。慢速方面与网站和移动设备不兼容。

检测对象的第二种流行方法是使用另一个时髦词汇,“完全卷积”方法(有关卷积的更多信息,请参阅第十章)。这些方法没有深度神经网络,这就是为什么它们避免需要特定的输入尺寸。没错,您不需要为完全卷积方法调整图像大小,而且它们也很快。

这就是 SSD MobileNet 中的“SSD”之所以重要的地方。它代表单次检测器。是的,您和我可能一直在想固态驱动器,但命名事物可能很困难,所以我们将数据科学放过。SSD 模型类型被设计为完全卷积模型,一次性识别图像的特征。这种“单次检测”使 SSD 比 R-CNN 快得多。不深入细节,SSD 模型有两个主要组件,一个骨干模型,它了解如何识别对象,以及一个SSD 头部,用于定位对象。在这种情况下,骨干是快速友好的 MobileNet。

结合 MobileNet 和 SSD 需要一点魔法,称为控制流,它允许您在模型中有条件地运行操作。这就是使predict方法从简单变得需要异步调用executeAsync的原因。当模型实现控制流时,同步的predict方法将无法工作。

条件逻辑通常由本地语言处理,但这会显著减慢速度。虽然大多数 TensorFlow.js 可以通过利用 GPU 或 Web Assembly(WASM)后端进行优化,但 JavaScript 中的条件语句需要卸载优化张量并重新加载它们。SSD MobileNet 模型为您隐藏了这个头疼的问题,只需使用控制流操作的低成本。虽然实现控制流超出了本书的范围,但使用这些高级功能的模型并不是。

由于这个模型的现代性,它不是为处理图像批次而设置的。这意味着输入的唯一限制不是图像大小,而是批量大小。但是,它确实期望一个批量为 1,因此一个 1,024×768 的 RGB 图像将以[1, 768, 1024, 3]的形式输入到该模型中,其中1是批量的堆栈大小,768是图像高度,1024是图像宽度,3是每个像素的 RGB 值。

深入了解您将处理的输入和输出类型非常重要。值得注意的是,模型的输出边界框遵循输入的经典高度和宽度架构,与宠物面部检测器不同。这意味着边界框将是[y1, x1, y2, x2]而不是[x1, y1, x2, y2]。如果不注意到这些小问题,可能会非常令人沮丧。您的边界框看起来会完全错乱。每当您实现一个新模型时,重要的是您从所有可用的文档中验证规范。

在深入代码之前还有一个注意事项。根据我的经验,生产中的目标检测很少用于识别成千上万种不同的类别,就像您在 MobileNet 和 Inception 中看到的那样。这样做有很多很好的理由,因此目标检测通常在少数类别上进行测试和训练。人们用于目标检测训练的一个常见组标记数据是Microsoft Common Objects in Context(COCO)数据集。这个 SSD MobileNet 使用了该数据集来教会模型看到 80 种不同的类别。虽然 80 种类别比 1,001 种可能的类别要少很多,但仍然是一个令人印象深刻的集合。

现在您对 SSD MobileNet 的了解比大多数使用它的人更多。您知道它是一个使用控制流将 MobileNet 速度与 80 个类别的 SSD 结果联系起来的目标检测模型。这些知识将帮助您以后解释模型的结果。

边界框输出

现在您了解了模型,可以获得结果。在这个模型中,executeAsync返回的值是两个张量堆栈的普通 JavaScript 数组。第一个张量堆栈是检测到的内容,第二个张量堆栈是每个检测的边界框堆栈,换句话说,分数和它们的框。

阅读模型输出

你可以通过几行代码查看图像的结果。以下代码就是这样做的,也可以在本章的源代码中找到:

tf.ready().then(() => {
  const modelPath =
    "https://tfhub.dev/tensorflow/tfjs-model/ssd_mobilenet_v2/1/default/1"; // '// ①
  tf.tidy(() => {
    tf.loadGraphModel(modelPath, { fromTFHub: true }).then((model) => {
      const mysteryImage = document.getElementById("mystery");
      const myTensor = tf.browser.fromPixels(mysteryImage);
      // SSD Mobilenet batch of 1
      const singleBatch = tf.expandDims(myTensor, 0); // ②

      model.executeAsync(singleBatch).then((result) => {
        console.log("First", result[0].shape); // ③
        result[0].print();
        console.log("Second", result[1].shape); // ④
        result[1].print();
      });
    });
  });
});

这是 JavaScript 模型的 TFHub URL。

输入在秩上扩展为一个批次,形状为[1, 高度, 宽度, 3]。

得到的张量是[1, 1917, 90],其中返回了 1,917 个检测结果,每行中的 90 个概率值加起来为 1。

张量的形状为[1, 1917, 4],为 1,917 个检测提供了边界框。

图 6-3 显示了模型的输出。

控制台中的 SSD MobileNet 输出

图 6-3。前一段代码的输出

你可能会惊讶地看到 90 个值而不是 80 个可能的类别。仍然只有 80 个可能的类别。该模型中的十个结果索引未被使用。

虽然看起来你已经完成了,但还有一些警告信号。正如你可能想象的那样,绘制 1,917 个边界框不会有用或有效,但是尝试一下看看。

显示所有输出

是时候编写代码来绘制多个边界框了。直觉告诉我们,1,917 个检测结果太多了。现在是时候编写一些代码来验证一下了。由于代码变得有点过于依赖 promise,现在是时候切换到 async/await 了。这将阻止代码进一步缩进,并提高可读性。如果你不熟悉在 promise 和 async/await 之间切换,请查看 JavaScript 的相关部分。

绘制模型检测的完整代码可以在书籍源代码文件too_many.html中找到。这段代码使用了与上一章节中对象定位部分描述的相同技术,但参数顺序已调整以适应模型的预期输出。

const results = await model.executeAsync(readyfied);
const boxes = await results[1].squeeze().array();

// Prep Canvas
const detection = document.getElementById("detection");
const ctx = detection.getContext("2d");
const imgWidth = mysteryImage.width;
const imgHeight = mysteryImage.height;
detection.width = imgWidth;
detection.height = imgHeight;

boxes.forEach((box, idx) => {
  ctx.strokeStyle = "#0F0";
  ctx.lineWidth = 1;
  const startY = box[0] * imgHeight;
  const startX = box[1] * imgWidth;
  const height = (box[2] - box[0]) * imgHeight;
  const width = (box[3] - box[1]) * imgWidth;
  ctx.strokeRect(startX, startY, width, height);
});

无论模型的置信度如何,绘制每个检测并不困难,但结果输出完全无法使用,如图 6-4 所示。

检测结果过多

图 6-4。1,917 个边界框,使图像无用

在图 6-4 中看到的混乱表明有大量的检测结果,但没有清晰度。你能猜到是什么导致了这种噪音吗?导致你看到的噪音有两个因素。

检测清理

对结果边界框的第一个批评是没有质量或数量检查。代码没有检查检测值的概率或过滤最有信心的值。你可能不知道,模型可能只有 0.001%的确信度,那么微小的检测就不值得绘制边界框。清理的第一步是设置检测分数的最小阈值和最大边界框数量。

其次,在仔细检查后,绘制的边界框似乎一遍又一遍地检测到相同的对象,只是略有变化。稍后将对此进行验证。最好是当它们识别出相同类别时,它们的重叠应该受到限制。如果两个重叠的框都检测到一个人,只保留检测分数最高的那个。

模型在照片中找到了东西(或没有找到),现在轮到你来进行清理了。

质量检查

你需要最高排名的预测。你可以通过抑制低于给定分数的任何边界框来实现这一点。通过一次调用topk来识别整个检测系列中的最高分数,如下所示:

const prominentDetection = tf.topk(results[0]);
// Print it to be sure
prominentDetection.indices.print()
prominentDetection.values.print()

对所有检测结果调用topk将返回一个仅包含最佳结果的数组,因为k默认为1。每个检测的索引对应于类别,值是检测的置信度。输出看起来会像图 6-5。

整个批次的 Topk 检测日志

图 6-5。topk调用适用于整个批次

如果显著检测低于给定阈值,您可以拒绝绘制框。然后,您可以限制绘制的框,仅绘制前 N 个预测。我们将把这个练习的代码留给章节挑战,因为它无法解决第二个问题。仅仅进行质量检查会导致在最强预测周围出现一堆框,而不是单个预测。结果框看起来就像您的检测系统喝了太多咖啡(参见图 6-6)。

框重叠

图 6-6。绘制前 20 个预测会产生模糊的边框

幸运的是,有一种内置的方法来解决这些模糊的框,并为您的晚餐派对提供一些新术语。

IoUs 和 NMS

直到现在,您可能认为 IoUs 只是由 Lloyd Christmas 支持的一种获得批准的法定货币,但在目标检测和训练领域,它们代表交集与并集。交集与并集是用于识别对象检测器准确性和重叠的评估指标。准确性部分对于训练非常重要,而重叠部分对于清理重叠输出非常重要。

IoU 是用于确定两个框在重叠中共享多少面积的公式。如果框完全重叠,IoU 为 1,而它们的适合程度越低,数字就越接近零。标题“IoU”来自于这个计算公式。框的交集面积除以框的并集面积,如图 6-7 所示。

IoU 图

图 6-7。交集与并集

现在您有一个快速的公式来检查边界框的相似性。使用 IoU 公式,您可以执行一种称为非极大值抑制(NMS)的算法来消除重复。NMS 会自动获取得分最高的框,并拒绝任何 IoU 超过指定水平的相似框。图 6-8 展示了一个包含三个得分框的简单示例。

非极大值抑制图

图 6-8。只有最大值存活;其他得分较低的框被移除

如果将 NMS 的 IoU 设置为 0.5,则任何与得分更高的框共享 50%面积的框将被删除。这对于消除与同一对象重叠的框非常有效。但是,对于彼此重叠并应该有两个边界框的两个对象可能会出现问题。这对于具有不幸角度的真实对象是一个问题,因为它们的边界框将相互抵消,您只会得到两个实际对象的一个检测。对于这种情况,您可以启用一个名为Soft-NMS的 NMS 的高级版本,它将降低重叠框的分数而不是删除它们。如果它们在被降低后的分数仍然足够高,检测结果将存活并获得自己的边界框,即使 IoU 非常高。图 6-9 使用 Soft-NMS 正确识别了与极高交集的两个对象。

Soft-NMS 示例

图 6-9。即使是真实世界中重叠的对象也可以使用 Soft-NMS 进行检测

Soft-NMS 最好的部分是它内置在 TensorFlow.js 中。我建议您为所有目标检测需求使用这个 TensorFlow.js 函数。在这个练习中,您将使用内置的方法,名为tf.image.nonMaxSuppressionWithScoreAsync。TensorFlow.js 内置了许多 NMS 算法,但tf.image.nonMaxSuppressionWithScoreAsync具有两个优点,使其非常适合使用:

  • WithScore提供 Soft-NMS 支持。

  • Async可以阻止 GPU 锁定 UI 线程。

在使用非异步高级方法时要小心,因为它们可能会锁定整个 UI。如果出于任何原因想要移除 Soft-NMS 方面,可以将最后一个参数(Soft-NMS Sigma)设置为零,然后您就得到了传统的 NMS。

const nmsDetections = await tf.image.nonMaxSuppressionWithScoreAsync(
  justBoxes, // shape [numBoxes, 4]
  justValues, // shape [numBoxes]
  maxBoxes, // Stop making boxes when this number is hit
  iouThreshold, // Allowed overlap value 0 to 1
  detectionThreshold, // Minimum detection score allowed
  1 // 0 is normal NMS, 1 is max Soft-NMS
);

只需几行代码,您就可以将 SSD 结果澄清为几个清晰的检测结果。

结果将是一个具有两个属性的对象。selectedIndices属性将是一个张量,其中包含通过筛选的框的索引,selectedScores将是它们对应的分数。您可以循环遍历所选结果并绘制边界框。

const chosen = await nmsDetections.selectedIndices.data(); // ①
chosen.forEach((detection) => {
  ctx.strokeStyle = "#0F0";
  ctx.lineWidth = 4;
  const detectedIndex = maxIndices[detection]; // ②
  const detectedClass = CLASSES[detectedIndex]; // ③
  const detectedScore = scores[detection];
  const dBox = boxes[detection];
  console.log(detectedClass, detectedScore); // ④

  // No negative values for start positions
  const startY = dBox[0] > 0 ? dBox[0] * imgHeight : 0; // ⑤
  const startX = dBox[1] > 0 ? dBox[1] * imgWidth : 0;
  const height = (dBox[2] - dBox[0]) * imgHeight;
  const width = (dBox[3] - dBox[1]) * imgWidth;
  ctx.strokeRect(startX, startY, width, height);
});

从结果中高得分的框的索引创建一个普通的 JavaScript 数组。

从先前的topk调用中获取最高得分的索引。

将类别作为数组导入以匹配给定的结果索引。这种结构就像上一章中 Inception 示例中的代码一样。

记录在画布中被框定的内容,以便验证结果。

禁止负数,以便框至少从帧开始。否则,一些框将从左上角被切断。

返回的检测数量各不相同,但受限于 NMS 中设置的规格。示例代码导致了五个正确的检测结果,如图 6-10 所示。

非最大抑制结果

图 6-10。干净的 Soft-NMS 检测结果

循环中的控制台日志打印出五个检测结果分别为三个“人”检测、一个“酒杯”和一个“餐桌”。将图 6-11 中的五个日志与图 6-10 中的五个边界框进行比较。

非最大抑制结果日志

图 6-11。结果日志类别和置信水平

UI 已经取得了很大进展。覆盖层应该能够识别检测和它们的置信度百分比是合理的。普通用户不知道要查看控制台以查看日志。

添加文本覆盖

您可以以各种花式方式向画布添加文本,并使其识别相关的边界框。在此演示中,我们将回顾最简单的方法,并将更美观的布局留给读者作为任务。

可以使用画布 2D 上下文的fillText方法向画布绘制文本。您可以通过重复使用绘制框时使用的X, Y坐标将文本定位在每个框的左上角。

有两个绘制文本时需要注意的问题:

  • 文本与背景之间很容易出现低对比度。

  • 与同时绘制的框相比,同时绘制的文本可能会被后绘制的框覆盖。

幸运的是,这两个问题都很容易解决。

解决低对比度

创建可读标签的典型方法是绘制一个背景框,然后放置文本。正如您所知,strokeRect创建一个没有填充颜色的框,所以不应该感到意外的是fillRect绘制一个带有填充颜色的框。

矩形应该有多大?一个简单的答案是将矩形绘制到检测框的宽度,但不能保证框足够宽,当框非常宽时,这会在结果中创建大的阻挡条。唯一有效的解决方案是测量文本并相应地绘制框。文本高度可以通过利用上下文font属性来设置,宽度可以通过measureText确定。

最后,您可能需要考虑从绘图位置减去字体高度,以便将文本绘制在框内而不是在框上方,但上下文已经有一个属性可以设置以保持简单。context.textBaseline属性有各种选项。图 6-12 显示了每个可能属性选项的起始点。

文本基线选项

图 6-12。将textBaseline设置为top可以保持文本在 X 和 Y 坐标内

现在你知道如何绘制一个填充矩形到适当的大小并将标签放在内部。您可以将这些方法结合在您的forEach循环中,您在其中绘制检测结果。标签绘制在每个检测的左上角,如图 6-13 所示。

显示绘制结果

图 6-13。标签与每个框一起绘制

重要的是文本在背景框之后绘制,否则框将覆盖文本。对于我们的目的,标签将使用略有不同颜色的绿色绘制,而不是边界框。

// Draw the label background.
ctx.fillStyle = "#0B0";
ctx.font = "16px sans-serif"; // ①
ctx.textBaseline = "top"; // ②
const textHeight = 16;
const textPad = 4; // ③
const label = `${detectedClass} ${Math.round(detectedScore * 100)}%`;
const textWidth = ctx.measureText(label).width;
ctx.fillRect(  // ④
  startX,
  startY,
  textWidth + textPad,
  textHeight + textPad
);
// Draw the text last to ensure it's on top.
ctx.fillStyle = "#000000";  // ⑤
ctx.fillText(label, startX, startY); // ⑥

设置标签使用的字体和大小。

设置textBaseline如上所述。

添加一点水平填充以在fillRect渲染中使用。

使用相同的startXstartY绘制矩形,这与绘制边界框时使用的相同。

fillStyle更改为黑色以进行文本渲染。

最后,绘制文本。这可能也应该略微填充。

现在每个检测都有一个几乎可读的标签。但是,根据您的图像,您可能已经注意到了一些问题,我们现在将解决。

解决绘制顺序

尽管标签是绘制在框的上方,但框是在不同的时间绘制的,可以轻松重叠一些现有标签文本,使它们难以阅读甚至不可能阅读。如您在图 6-14 中所见,由于重叠检测,餐桌百分比很难阅读。

上下文重叠问题

图 6-14。上下文绘制顺序重叠问题

解决这个问题的一种方法是遍历检测结果并绘制框,然后再进行第二次遍历并绘制文本。这将确保文本最后绘制,但代价是需要在两个连续循环中遍历检测结果。

作为替代方案,您可以使用代码处理这个问题。您可以设置上下文globalCompositeOperation来执行各种令人惊奇的操作。一个简单的操作是告诉上下文在现有内容的上方或下方渲染,有效地设置 z 顺序。

strokeRect调用可以设置为globalCompositeOperationdestination-over。这意味着任何存在于目标中的像素将获胜并放置在添加的内容上方。这有效地在任何现有内容下绘制。

然后,在绘制标签时,将globalCompositionOperation返回到其默认行为,即source-over。这会将新的源像素绘制在任何现有绘图上。如果在这两种操作之间来回切换,您可以确保您的标签是最优先的,并在主循环内处理所有内容。

总的来说,绘制边界框、标签框和标签的单个循环如下所示:

chosen.forEach((detection) => {
  ctx.strokeStyle = "#0F0";
  ctx.lineWidth = 4;
  ctx.globalCompositeOperation='destination-over'; // ①
  const detectedIndex = maxIndices[detection];
  const detectedClass = CLASSES[detectedIndex];
  const detectedScore = scores[detection];
  const dBox = boxes[detection];

  // No negative values for start positions
  const startY = dBox[0] > 0 ? dBox[0] * imgHeight : 0;
  const startX = dBox[1] > 0 ? dBox[1] * imgWidth : 0;
  const height = (dBox[2] - dBox[0]) * imgHeight;
  const width = (dBox[3] - dBox[1]) * imgWidth;
  ctx.strokeRect(startX, startY, width, height);
  // Draw the label background.
  ctx.globalCompositeOperation='source-over'; // ②
  ctx.fillStyle = "#0B0";
  const textHeight = 16;
  const textPad = 4;
  const label = `${detectedClass} ${Math.round(detectedScore * 100)}%`;
  const textWidth = ctx.measureText(label).width;
  ctx.fillRect(
    startX,
    startY,
    textWidth + textPad,
    textHeight + textPad
  );
  // Draw the text last to ensure it's on top.
  ctx.fillStyle = "#000000";
  ctx.fillText(label, startX, startY);
});

在任何现有内容下绘制。

在任何现有内容上绘制。

结果是一个动态的人类可读结果,您可以与您的朋友分享(参见图 6-15)。

完全工作的目标检测

图 6-15。使用destination-over修复重叠问题

连接到网络摄像头

所有这些速度的好处是什么?正如前面提到的,选择 SSD 而不是 R-CNN,选择 MobileNet 而不是 Inception,以及一次绘制画布而不是两次。当你加载页面时,它看起来相当慢。似乎至少需要四秒才能加载和渲染。

是的,把一切都放在适当的位置需要一点时间,但在内存分配完毕并且模型下载完成后,你会看到一些相当显著的速度。是的,足以在你的网络摄像头上运行实时检测。

加快流程的关键是运行设置代码一次,然后继续运行检测循环。这意味着你需要将这节课的庞大代码库分解;否则,你将得到一个无法使用的界面。为简单起见,你可以按照示例 6-1 中所示分解项目。

示例 6-1。分解代码库
async function doStuff() {
  try {
    const model = await loadModel()  // ①
    const mysteryVideo = document.getElementById('mystery') // ②
    const camDetails = await setupWebcam(mysteryVideo) // ③
    performDetections(model, mysteryVideo, camDetails) // ④
  } catch (e) {
    console.error(e) // ⑤
  }
}

加载模型时最长的延迟应该首先发生,且仅发生一次。

为了效率,你可以一次捕获视频元素,并将该引用传递到需要的地方。

设置网络摄像头应该只发生一次。

performDetections方法可以在检测网络摄像头中的内容并绘制框时无限循环。

不要让所有这些awaits吞没错误。

从图像到视频的转换

从静态图像转换为视频实际上并不复杂,因为将所见内容转换为张量的困难部分由tf.fromPixels处理。tf.fromPixels方法可以读取画布、图像,甚至视频元素。因此,复杂性在于将img标签更改为video标签。

你可以通过更换标签来开始。原始的img标签:

<img id="mystery" src="/dinner.jpg" height="100%" />

变成以下内容:

<video id="mystery" height="100%" autoplay></video>

值得注意的是,视频元素的宽度/高度属性稍微复杂,因为有输入视频的宽度/高度和实际客户端的宽度/高度。因此,所有使用width的计算都需要使用clientWidth,同样,height需要使用clientHeight。如果使用错误的属性,框将不对齐,甚至可能根本不显示。

激活网络摄像头

为了我们的目的,我们只会设置默认的网络摄像头。这对应于示例 6-1 中的第四点。如果你对getUserMedia不熟悉,请花点时间分析视频元素如何连接到网络摄像头。这也是你可以将画布上下文设置移动到适应视频元素的时间。

async function setupWebcam(videoRef) {
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    const webcamStream = await navigator.mediaDevices.getUserMedia({ // ①
      audio: false,
      video: {
        facingMode: 'user',
      },
    })

    if ('srcObject' in videoRef) { // ②
      videoRef.srcObject = webcamStream
    } else {
      videoRef.src = window.URL.createObjectURL(webcamStream)
    }

    return new Promise((resolve, _) => {  // ③
      videoRef.onloadedmetadata = () => { // ④
        // Prep Canvas
        const detection = document.getElementById('detection')
        const ctx = detection.getContext('2d')
        const imgWidth = videoRef.clientWidth // ⑤
        const imgHeight = videoRef.clientHeight
        detection.width = imgWidth
        detection.height = imgHeight
        ctx.font = '16px sans-serif'
        ctx.textBaseline = 'top'
        resolve([ctx, imgHeight, imgWidth]) // ⑥
      }
    })
  } else {
    alert('No webcam - sorry!')
  }
}

这些是网络摄像头用户媒体配置约束。这里可以应用几个选项,但为简单起见,保持得很简单。

这个条件检查是为了支持不支持新的srcObject配置的旧浏览器。根据你的支持需求,这可能会被弃用。

在视频加载完成之前无法访问视频,因此该事件被包装在一个 promise 中,以便等待。

这是你需要等待的事件,然后才能将视频元素传递给tf.fromPixels

在设置画布时,注意使用clientWidth而不是width

promise 解析后,你将需要将信息传递给检测和绘制循环。

绘制检测结果

最后,您执行检测和绘图的方式与对图像执行的方式相同。在每次调用的开始时,您需要删除上一次调用的所有检测;否则,您的画布将慢慢填满旧的检测。清除画布很简单;您可以使用clearRect来删除指定坐标的任何内容。传递整个画布的宽度和高度将擦除所有内容。

ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)

在每次绘制检测结束时,不要在清理中处理模型,因为您需要在每次检测中使用它。然而,其他所有内容都可以和应该被处理。

在示例 6-1 中确定的performDetections函数应该在无限循环中递归调用自身。该函数的循环速度可能比画布绘制速度更快。为了确保不浪费循环,使用浏览器的requestAnimationFrame来限制这一点:

// Loop forever
requestAnimationFrame(() => {
  performDetections(model, videoRef, camDetails)
})

就是这样。通过一些逻辑调整,您已经从静态图像转移到了实时速度的视频输入。在我的电脑上,我看到大约每秒 16 帧。在人工智能领域,这已经足够快,可以处理大多数用例。我用它来证明我至少是 97%的人,如图 6-16 所示。

在浏览器中运行的 SSD MobileNet 的屏幕截图

图 6-16。具有 SSD MobileNet 的完全功能网络摄像头

章节回顾

祝贺您挑战了 TensorFlow Hub 上存在的最有用但也最复杂的模型之一。虽然用 JavaScript 隐藏这个模型的复杂性很简单,但您现在熟悉了一些最令人印象深刻的物体检测和澄清概念。机器学习背负着快速解决问题的概念,然后解决后续代码以将 AI 的壮丽属性附加到给定领域。您可以期待任何显著先进模型和领域都需要大量研究。

章节挑战:顶级侦探

NMS 简化了排序和消除检测。假设您想解决识别顶级预测然后将它们从高到低排序的问题,以便您可以创建类似图 6-6 的图形。与其依赖 NMS 来找到您最可行和最高值,您需要自己解决最高值问题。将这个小但类似的分组视为整个检测数据集。想象这个[1, 6, 5]的张量检测集合是您的result[0],您只想要具有最高置信度值的前三个检测。您如何解决这个问题?

  const t = tf.tensor([[
    [1, 2, 3, 4, 5],
    [1.1, 2.1, 3.1, 4.1, 5.1],
    [1.2, 2.2, 3.2, 4.2, 5.2],
    [1.2, 12.2, 3.2, 4.2, 5.2],
    [1.3, 2.3, 3.3, 4.3, 5.3],
    [1, 1, 1, 1, 1]
  ]])

  // Get the top-three most confident predictions.

您的最终解决方案应该打印[3, 4, 2],因为索引为 3 的张量具有最大值(12.2),其次是索引为 4(包含 5.3),然后是索引为 2(5.2)。

您可以在附录 B 中找到此挑战的答案。

复习问题

让我们回顾您在本章编写的代码中学到的知识。花点时间回答以下问题:

  1. 在物体检测机器学习领域,SSD 代表什么?

  2. 您需要使用哪种方法来预测使用动态控制流操作的模型?

  3. SSD MobileNet 预测多少类别和多少值?

  4. 去重相同对象的检测的方法是什么?

  5. 使用大型同步 TensorFlow.js 调用的缺点是什么?

  6. 您应该使用什么方法来识别标签的宽度?

  7. globalCompositeOperation会覆盖画布上现有的内容吗?

这些问题的解决方案可以在附录 A 中找到。

第七章:模型制作资源

“通过寻找和失误我们学习。”

—约翰·沃尔夫冈·冯·歌德

你不仅限于来自 TensorFlow Hub 的模型。每天都有新的令人兴奋的模型被推文、发表和在社区中受到关注。这些模型和想法在谷歌认可的中心之外分享,有时甚至超出了 TensorFlow.js 的范围。

你开始超越园墙,与野外的模型和数据一起工作。这一章专门旨在为你提供制作现有模型的新方法,并让你面对收集和理解数据的挑战。

我们将:

  • 介绍模型转换

  • 介绍 Teachable Machine

  • 训练一个计算机视觉模型

  • 回顾训练数据的来源

  • 涵盖一些关键的训练概念

当你完成这一章时,你将掌握几种制作模型的方法,并更好地理解使用数据制作机器学习解决方案的过程。

网络外模型购物

TensorFlow.js 并没有存在很长时间。因此,可用的模型数量有限,或者至少比其他框架少。这并不意味着你没有机会。你通常可以将在其他框架上训练过的模型转换为 TensorFlow.js。将现有模型转换为在新环境中工作的新模型是一种发现最近开发的资源并创建令人兴奋和现代的模型的好方法。

模型动物园

从机器学习世界中出现的一个有点可爱的术语是,有时将一组模型称为动物园。这些模型动物园是各种给定框架的任务的模型宝库,就像 TensorFlow Hub 一样。

模型动物园是一个绝佳的地方,可以找到独特的模型,这些模型可能会激发或满足你的需求。动物园经常链接到已发表的作品,解释了为模型架构和用于创建它们的数据所做的选择。

真正的好处来自于这样一个原则,一旦你学会了如何将这些模型转换为 TensorFlow.js,你可能会转换很多模型。

值得花一点时间回顾转换模型,这样你就能理解每个模型动物园或已发表模型对 TensorFlow.js 可能有多容易访问。

转换模型

许多用 Python 编程的 TensorFlow 模型以一种称为 Keras HDF5 的格式保存。HDF5 代表分层数据格式 v5,但通常被称为 Keras 或仅为 h5 文件。这种文件格式是一个带有 h5 扩展名的文件。Keras 文件格式中包含大量数据:

  • 指定模型层的架构

  • 一组权重值,类似于 bin 文件

  • 模型的优化器和损失指标

这是更受欢迎的模型格式之一,更重要的是,即使它们是用 Python 训练的,它们也很容易转换为 TensorFlow.js。

注意

有了能够转换 TensorFlow Keras 模型的知识,这意味着你找到的任何 TensorFlow 教程都可以作为一个教程来阅读,最终产品很可能可以在 TensorFlow.js 中使用。

运行转换命令

要从 h5 转换为 TensorFlow.js model.json和 bin 文件,你需要tfjs-convertertfjs-converter还可以转换除 HDF5 之外的其他 TensorFlow 模型类型,因此它是处理任何 TensorFlow 到 TensorFlow.js 格式的绝佳工具。

转换器要求你的计算机已设置 Python。使用pip安装转换器。pip命令是 Python 的软件包安装程序,类似于 JavaScript 中的npm。如果你的计算机还没有准备好,有大量关于安装 Python 和pip的教程。安装了pip和 Python 后,你可以运行tfjs-converter

这是转换器的安装命令:

$ pip install tensorflowjs[wizard]

这将安装两个东西:一个无废话的转换器,您可以在自动化中使用(tensorflowjs_converter),以及一个通过键入tensorflowjs_wizard来运行的向导转换器。对于我们的目的,我建议使用向导界面进行转换,这样您可以利用新功能。

您可以通过在命令行中调用您新安装的tensorflowjs_wizard命令来运行向导,然后您将被提示类似于您在图 7-1 中看到的问题。

示例转换向导

图 7-1. 向导开始询问问题

这个向导将询问您的输入模型格式和所需的输出模型格式。根据您的答案,它还会询问一些问题。虽然向导将继续更新,但在选择所需设置时,这里有一些概念您应该牢记:

在图/层模型之间进行选择

请记住,图模型更快,但缺少一些内省和自定义属性,这些属性由层模型提供。

压缩(通过量化)

这将使您的模型从存储 32 位精度权重降至 16 位甚至 8 位精度权重值。使用更少的位数意味着您的模型在可能牺牲精度的情况下更小。量化后,您应该重新测试您的模型。大多数情况下,这种压缩对于客户端模型是值得的。

分片大小

建议的分片大小是为了优化您的模型以用于客户端浏览器缓存。除非您不在客户端浏览器中使用该模型,否则应保持推荐的大小。

注意

量化仅影响磁盘上的模型大小。这为网站提供了显著的网络传输优势,但当模型加载到 RAM 中时,值将返回到当前 TensorFlow.js 中的 32 位变量。

功能将继续出现在向导界面中。如果出现了一个让您困惑的新功能,请记住,转换模型的文档将在tfjs-converter README 源代码中提供。您的体验将类似于图 7-2。

示例转换

图 7-2. Windows 上的示例向导演练

生成的文件夹包含一个转换后的 TensorFlow.js 模型,准备就绪。h5 文件现在是model.json,而可缓存的 bin 文件以块形式存在。您可以在图 7-3 中看到转换结果。

示例转换

图 7-3. TensorFlow.js 模型结果

中间模型

如果您找到一个想要转换为 TensorFlow.js 的模型,现在可以检查是否有转换器将该模型移动到 Keras HDF5 格式,然后您就可以将其转换为 TensorFlow.js。值得注意的是,目前正在大力努力将模型转换为和从一个称为开放神经网络交换(ONNX)的格式标准化。目前,微软和许多其他合作伙伴正在努力正确地转换模型进出 ONNX 格式,这将允许一个独立于框架的模型格式。

如果您找到了一个已发布的模型,想要在 TensorFlow.js 中使用,但它并不是在 TensorFlow 中训练的,不要放弃希望。您应该检查该模型类型是否有 ONNX 支持。

有些模型无法直接转换为 TensorFlow,因此您可能需要通过其他转换服务进行更多的迂回路线。除了 TensorFlow 之外,另一个受欢迎的框架库是 PyTorch,大多数机器学习爱好者使用。虽然 ONNX 每天都在变得更接近,但目前从 PyTorch 转换为 TensorFlow.js 的最佳方法是通过一系列工具进行转换,如图 7-4 所示。

转换模型的流程图

图 7-4. 转换模型

虽然对模型进行转换可能看起来是一项艰巨的工作,但将现有格式的模型转换为 TensorFlow.js 可以节省您几天甚至几周的时间,而不必重新创建和重新训练模型以适应已发布的数据。

您的第一个定制模型

如果只需下载现有模型,那么您就完成了。但我们不能等待谷歌发布能够分类我们所需内容的模型。您可能有一个需要 AI 对糕点有深入了解的想法。即使是谷歌的 Inception v3,如果您需要区分单个领域中各种物品之间的区别,可能也不够强大。

幸运的是,有一个技巧可以让我们利用现有模型的成果。一些模型可以稍作调整,以分类新事物!我们不需要重新训练整个模型,只需训练最后几层以寻找不同的特征。这使我们能够将像 Inception 或 MobileNet 这样的高级模型转变为识别我们想要的东西的模型。作为一个额外的好处,这种方法允许我们用极少量的数据重新训练模型。这被称为迁移学习,是在新类别上(重新)训练模型的最常见方法之一。

我们将在第十一章中介绍迁移学习的代码,但现在您也可以体验它。谷歌为人们尝试训练模型构建了一个完整的迁移学习 UI。

见见 Teachable Machine

首先,您将使用谷歌提供的一个名为 Teachable Machine 的工具。这个工具是由 TensorFlow.js 提供支持的简单网站,它允许您上传图像、上传音频,甚至使用网络摄像头进行训练、捕获数据和创建 TensorFlow.js 模型。这些模型直接在您的浏览器中进行训练,然后为您托管,以便您立即尝试您的代码。您得到的模型是 MobileNet、PoseNet 或其他实用模型的迁移学习版本,适合您的需求。由于它使用迁移学习,您根本不需要太多数据。

警告

使用少量数据创建的模型似乎效果神奇,但存在显著的偏见。这意味着它们在训练时表现良好,但在背景、光线或位置变化时会出错。

训练模型的网站位于teachablemachine.withgoogle.com。访问该网站后,您可以开始各种项目,如音频、图像,甚至身体姿势。虽然您可以并且应该尝试每一个,但本书将介绍图像项目选项。这是图 7-5 中显示的第一个选项。

Teachable Machine UI

图 7-5. 令人惊叹的 Teachable Machine 选项

在生成的页面上,您可以选择上传或使用网络摄像头收集每个类别的样本图像。

以下是一些您可以使用的想法,用于创建您的第一个分类器:

  • 竖起大拇指还是竖起食指?

  • 我在喝水吗?

  • 这是哪只猫?

  • 秘密手势解锁某物?

  • 书还是香蕉!?

发挥您的创造力!您创建的任何模型都可以轻松展示给朋友和社交媒体,或者可以转变为一个帮助您的网页。例如,“我在喝水吗?”分类器可以连接到一个计时器,用于您的自我补水项目。只要您用一些样本训练模型,您可以想出各种有趣的项目。

就我个人而言,我将训练一个“爸爸在工作吗?”分类器。你们中的许多人可能在远程工作环境中遇到了家庭问题。如果我坐在桌子前,门是关着的,你会认为这会告诉别人我在工作,对吧?但如果门是开着的,“请进!”我会让 Teachable Machine 使用我的网络摄像头来分类我在工作时的样子和我不工作时的样子。

很酷的一点是,由于检测器将与网站绑定,“爸爸在工作吗?”可以扩展到做各种令人惊叹的事情。它可以发送短信,打开“不可用”灯,甚至告诉我的亚马逊 Echo 设备在被问及我是否在工作时回答“是”。只要我能制作一个快速可靠的 AI 图像分类器,就有无限的机会。

从头开始训练是一个可扩展的解决方案,但目前的任务是训练我在办公室的存在,为此我们将使用 Teachable Machine。

使用 Teachable Machine

让我们快速浏览一下使用 Teachable Machine 创建模型的用户界面。用户界面设置为网络图,信息从左到右自上而下填写。使用该网站很容易。跟随我们一起查看图 7-6。

Teachable Machine 用户界面导览

图 7-6。图像项目用户界面导览
  1. 这个上部标题应该很小,在较大的监视器上不会妨碍。从标题中,您可以使用 Google Drive 来管理您的数据和结果,这样您就可以从上次离开的地方继续或与他人分享您的模型训练。

  2. 顶部项目称为“类别 1”,表示您分类的一个类别。当然,您可以重命名它!我已将我的重命名为“工作”。在这个工作流卡中,您可以提供访问您的网络摄像头或上传符合此分类的图像文件。

  3. 这个第二个工作流卡是任何第二类。在我的示例中,这可能是“免费”或“不工作”。在这里,您提供适合您的次要分类的数据。

  4. 所有类别都进入训练工作流程。当您有要构建的示例时,您可以点击“训练模型”按钮并积极训练模型。当我们到达高级选项卡时,我们将深入了解这是如何进行的。

  5. 预览部分立即显示模型实时分类的结果。

收集数据和训练

您可以按住网络摄像头的“按住录制”按钮,立即提供数百张示例数据的图像。在您的数据集中尽可能评估和包含变化是至关重要的。例如,如果您正在做“竖起大拇指还是竖下大拇指”,重要的是您在屏幕周围移动手部,捕捉不同角度,并将手放在面部、衬衫和任何其他复杂背景前。

对我来说,我调整了我的照明,因为有时我有一个摄像头主灯,有时我有背光。几秒钟内,我有了数百种不同条件的办公室门开着和关着的照片。我甚至拍了一些照片,我的门是关着的,但我没有坐在桌子前。

Teachable Machine 的一个很棒的地方是它可以在浏览器中快速给出结果,因此如果模型需要更多数据,您可以随时回来并立即添加更多数据。

一旦您有几百张照片,您可以点击“训练模型”按钮,您将看到一个“训练…”进度图(参见图 7-7)。

训练截图

图 7-7。Teachable Machine 活动训练

那么现在发生了什么?简而言之,Teachable Machine 正在使用您的图像执行迁移学习来重新训练 MobileNet 模型。您的数据中随机选择了 85%用于训练模型,另外 15%用于测试模型的性能。

点击高级选项卡查看此特定配置的详细信息。这将暴露出通常称为超参数的机器学习训练的一些内容(参见图 7-8)。这些超参数是可调整的模型训练参数。

训练的可调参数

图 7-8。Teachable Machine 超参数

在这里,您将看到一些新术语。虽然现在学习这些术语并不是必要的,但您最终需要学习它们,因此我们将快速介绍它们。当您开始编写自己的模型时,每个概念都会出现在第八章中。

纪元

如果您来自编码背景,尤其是 JavaScript 编码,那么纪元是 1970 年 1 月 1 日。这 不是 在这个领域中纪元的含义。在机器学习训练中,一个纪元是对训练数据的完整遍历。在一个纪元结束时,AI 至少已经看到了所有的训练数据一次。五十个纪元意味着模型将不得不看到数据 50 次。一个很好的类比是单词卡。这个数字表示您与模型一起浏览整个单词卡组的次数,以便它学习。

批量大小

模型是以加载到内存中的批次进行训练的。有了几百张照片,您可以轻松处理所有图像,但最好按合理的增量进行批处理。

学习率

学习率影响机器学习模型在每次预测时应该如何调整。您可能会认为更高的学习率总是更好,但您会错。有时,特别是在微调迁移学习模型时,关键在于细节(如第十一章中所述)。

卡片底部还有一个带有“Under the hood”文本的按钮,点击它将为您提供有关训练模型进度的详细信息。随时查看报告。您将在以后实施这些指标。

验证模型

一旦 Teachable Machine 完成,它会立即将模型连接到您的网络摄像头,并显示模型的预测结果。这是一个很好的机会让您测试模型的结果。

对我来说,当我坐在办公桌前,门是关着的时候,模型预测我正在工作。万岁!我有一个可用的模型准备好了。两个类的表现都非常出色,如图 7-9 所示。

显示模型工作的预览部分

图 7-9。模型运行

理想情况下,您的训练进展顺利。现在,检索训练好的模型是至关重要的,这样它就可以在您更广泛的项目中实施。如果您想与朋友分享您的模型,可以在预览中点击“导出模型”按钮,您将获得各种选项。新的模态窗口提供了在 TensorFlow、TensorFlow Lite 和 TensorFlow.js 中应用您的模型的路径。甚至还有一个选项可以免费托管您的训练模型,而不是自己下载和托管模型。我们提供了所有这些友好的选项以及一些巧妙的复制粘贴代码,让您快速实施这些模型。导出代码屏幕应该类似于图 7-10。

Teachable Machine 导出对话框。

图 7-10。Teachable Machine 导出选项

当您的模型被下载或发布时,您的数据不会随之发布。要保存数据集,您需要将项目保存在 Google Drive 中。如果您计划随着时间推进模型或扩大数据集,请记住这一点。识别和处理边缘情况是数据科学过程的一部分。

在 Teachable Machine 的复制粘贴部分提供的免费代码隐藏了名为@teachablemachine/image的 NPM 包中的网络摄像头和张量的细节。虽然这对于不了解网络摄像头和张量的人来说很好,但对于最终产品来说却毫无用处。您从第六章中获得的高级 UI 技能使您的创造潜力远远超过了复制粘贴代码选项。

提示

每个 Teachable Machine 模型都是不同的;您刚刚训练的视觉模型是建立在我们的老朋友 MobileNet 分类器之上的。因此,当您实施模型时,将输入调整为 224 x 224。

你刚刚训练了你的第一个模型。然而,我们尽可能地省略了很多步骤。使用用户界面训练模型将成为机器学习的一个重要部分,它可以帮助每个新手获得一个出色的开始。但像你这样的张量巫师可以训练一个更加动态的模型。你显然希望通过编写一些 JavaScript 命令你的机器。所以让我们开始通过编写一些 JavaScript 来训练一个模型。

机器学习陷阱

在编码时,任何开发人员可能会面临各种问题。尽管编程语言各不相同,但有一组核心的陷阱会延续到每个基础设施。机器学习也不例外。虽然可能会有特定于任何选择的类型和问题的问题,但早期识别这些问题很重要,这样你就可以发现数据驱动算法中最常见的一些复杂问题。

我们现在将快速阐述一些概念,但每个概念在本书的其余部分涉及到工作时都会重新讨论:

  • 少量数据

  • 糟糕的数据

  • 数据偏差

  • 过拟合

  • 欠拟合

让我们回顾一下这些,这样我们就可以在接下来的章节中留意它们。

少量数据

有人给我提出了一个关于机器学习解决方案的绝妙想法,他们有三个标记样本。这个世界上很少有东西能从这么小的训练集中受益。当数据是你训练算法的方式时,你需要相当数量的数据。有多少?没有一个适用于每个问题的答案,但你应该倾向于更多的数据而不是更少。

糟糕的数据

有些人的生活干净、有序,但在现实世界中,数据不会无意中变得如此。如果你的数据缺失、标记错误,或者完全不合理,它可能会在训练中造成问题。很多时候,数据需要被清理,异常值需要被移除。准备好数据只是一个重要且关键的步骤。

数据偏差

你的数据可能被清晰地标记,每个细节都在正确的位置,但它可能缺少使其在实际情况下工作的信息。在某些情况下,这可能会引起严重的道德问题,而在其他情况下,这可能会导致你的模型在各种条件下表现不佳。例如,我之前训练的“爸爸在工作吗?”模型(图 7-9)可能不适用于其他人的办公室配置,因为数据只针对我的办公室。

过拟合

有时模型被训练到只在训练集数据上表现良好。在某些情况下,一个更直接但得分较低的准确性可能更好地适应新数据点。

看看这个分离图在图 7-11 中是如何过拟合数据的?虽然它完美地解决了给定的问题,但当添加新的从未见过的点时,它可能会变得更慢并失败。

过拟合图

图 7-11. 过拟合数据

有时你会听到过拟合被称为高方差,这意味着你在训练数据中的波动会导致模型在新数据上随机失败。

如果你的目标是让你的模型在新的、以前从未见过的数据上工作,过拟合可能是一个真正的问题。幸运的是,我们有测试和验证集来帮助。

欠拟合

如果你的模型没有被充分训练,或者它的结构无法适应数据,解决方案可能会失败,甚至完全偏离任何外推或额外数据。这是过拟合的反面,但在同样的意义上,它会产生一个糟糕的模型。

看看图 7-12 中的分离图是如何欠拟合数据的?

欠拟合图

图 7-12. 欠拟合数据

当模型欠拟合时,就说模型具有高偏差,因为对数据的基本假设实际上是错误的。虽然类似,但不要将这个术语与之前讨论的数据偏差混淆。

数据集购物

现在你明白为什么拥有多样化的数据是至关重要的。虽然 Teachable Machine 的“爸爸在工作吗?”模型对我很有用,但远远不够多样化,无法用于其他办公室。令人高兴的是,机器学习社区最令人印象深刻的一点是大家都愿意分享他们辛苦获得的数据集。

在收集数据之前,研究一下其他人是否已经发布了可用的标记数据是很有帮助的。了解专家机器学习数据集是如何组织的也是有益的。

数据集就像 JavaScript 库:一开始可能看起来很独特,但过一段时间后,你会发现同样的数据集一再被引用。世界各地的大学都有出色的有用数据集目录,甚至谷歌也有一个类似 TensorFlow Hub 的数据集托管服务,但没有一个能与 Kaggle 这个数据集居所相提并论。

Kaggle拥有大量各种类型的数据集。从鸟鸣到 IMDb 评论,你可以使用 Kaggle 提供的各种数据训练各种模型。图 7-13 展示了一个友好且可搜索的数据集界面。

你的章节挑战

图 7-13。Kaggle 提供了超过 60,000 个免费数据集

无论你是在研究用于训练模型的数据,还是在寻找使用机器学习制作新奇事物的想法,Kaggle 都能满足你的需求。

注意

Kaggle 不仅提供数据集,还是一个分享、竞争和赢取奖品的社区。

如果你对 Kaggle 的课外活动不感兴趣,你通常可以使用谷歌的数据集搜索网站,很可能会找到你的 Kaggle 数据集和其他数据集:https://datasetsearch.research.google.com

流行数据集

虽然数据集的列表每天都在增长,但很长一段时间内可供选择的数据集并不多。已发布的数据集很少,因此一些数据集成为训练示例的基础。其他数据集作为其类别的第一个发布,并不知不觉地成为某种机器学习的品牌大使。就像秘密口令一样,这些流行数据集在演讲和文档中被随意使用。了解一些最常见和著名的数据集是很有益的:

ImageNet

ImageNet 被用来训练一些最流行的计算机视觉模型。这个大型图像数据集一直被学术研究人员用来评估模型。

MNIST

这是一个 28 x 28 灰度手写数字的集合,用于训练一个读取数字的模型。它通常是计算机视觉模型的“Hello World”。其名称来源于其来源,即来自国家标准与技术研究所的修改数据集。

Iris

1936 年,罗纳德·费舍尔发现可以通过三个物理测量来识别鸢尾花的属种。这个数据集是非视觉分类的经典。

波士顿房价

这个数据集包含了中位数房屋价值及其相关属性,用于解决最佳拟合线(线性回归)模型。

泰坦尼克号

这是“不沉”RMS 泰坦尼克号于 1912 年 4 月 15 日沉没的乘客日志。我们将使用这个数据集在第九章中创建一个模型。

葡萄酒质量

对于酿酒师和手工艺者来说,利用机器学习来识别什么使一种美味饮料是令人振奋的。这个数据集包含每种葡萄酒的物理化学性质及其评分。

Pima 印第安人糖尿病患者

有很多健康护理数据集可用。这是一个基于患者病史的小型且易接近的糖尿病数据集。

CIFAR

虽然 ImageNet 是一个金标准,但有点难以接近和复杂。CIFAR 数据集是一个低分辨率且友好的图像集合,用于分类。

亚马逊评论

这是来自 Amazon.com 多年来的产品评论集合。该数据集已被用来训练文本的情感倾向,因为你有用户的评论和他们的评分。与此相近的是 IMDb 评论数据集。

COCO

这是一个大规模的目标检测、分割和字幕数据集。

这 10 个是标准参考数据集的很好起点。机器学习爱好者会在推特、演讲和博客文章中随意引用这些数据集。

章节复习

当然,你没有金星火山的各种照片。你怎么可能有呢?这并不意味着你不能拿一个为此训练过的模型,移植到你的新浏览器游戏中。只需从 Kaggle 下载数据集并上传图片到 Teachable Machine,创建一个体面的“火山还是非火山”天文模型。就像 TensorFlow.js 将你带入机器学习轨道一样,这些现有模型和数据集为你的应用程序掌握打下了基础。

像 Web 开发一样,机器学习包含各种专业化。机器学习依赖于数据、模型、训练和张量等多种技能。

章节挑战:再见 MNIST

现在轮到你将一个模型从 Keras HDF5 转换到 TensorFlow.js 了。在与本书相关的代码中,你会找到一个mnist.h5文件,其中包含了用于识别手写数字的模型。

  1. 创建一个图形 TensorFlow.js 模型。

  2. uint8量化模型使其变小。

  3. 使用通配符访问模型中的所有权重。

  4. 将分片大小设置为 12,000。

  5. 保存到一个文件夹./ministmin因为它被量化了,明白吗!)。

回答以下问题:

  1. 生成了多少个二进制文件和组?

  2. 最终输出大小是多少?

  3. 如果你使用默认的分片大小,会生成多少个二进制文件?

您可以在附录 B 中找到这个挑战的答案。

复习问题

让我们回顾一下你在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. 如果你为特定任务提供了大量数据,训练之前你会有哪些担忧和想法?

  2. 如果一个模型经过训练,准确率达到了 99%,但是当你在现场使用它时,表现很糟糕,你会说发生了什么?

  3. 谷歌创建的帮助您训练自己模型的网站的名称是什么?

  4. 使用谷歌网站的缺点是什么?

  5. 用来训练 MobileNet 和其他流行的机器学习模型的图像数据集是什么?

这些练习的解决方案可以在附录 A 中找到。

第八章:训练模型

“不要求负担更轻,而要求更宽广的肩膀。”

—犹太谚语

尽管令人印象深刻的模型和数据的供应将继续增长并溢出,但你可能希望做的不仅仅是消费 TensorFlow.js 模型。你将想出以前从未做过的想法,那天不会有现成的选择。现在是时候训练你自己的模型了。

是的,这是世界上最优秀的头脑竞争的任务。虽然关于训练模型的数学、策略和方法论可以写成一本书,但核心理解将至关重要。你必须熟悉使用 TensorFlow.js 训练模型的基本概念和好处,以充分利用这个框架。

我们将:

  • 用 JavaScript 代码训练你的第一个模型

  • 提升对模型架构的理解

  • 回顾如何在训练过程中跟踪状态

  • 涵盖一些训练的基本概念

当你完成这一章时,你将掌握几种训练模型的方法,并更好地理解使用数据制定机器学习解决方案的过程。

训练 101

现在是时候揭开魔法,用 JavaScript 训练一个模型了。虽然 Teachable Machine 是一个很好的工具,但它有限。要真正赋予机器学习力量,你需要确定你想要解决的问题,然后教会机器找到解决方案的模式。为了做到这一点,我们将通过数据的眼睛看问题。

在写任何代码之前,看看这个信息的例子,看看你能否确定这些数字之间的相关性。你有一个函数f,它接受一个数字并返回一个数字。以下是数据:

  • 给定-1,结果为-4。

  • 给定 0,结果为-2。

  • 给定 1,结果为 0。

  • 给定 2,结果为 2。

  • 给定 3,结果为 4。

  • 给定 4,结果为 6。

你能确定 5 的答案是什么吗?你能推断出 10 的解决方案吗?在继续之前花点时间评估数据。你们中的一些人可能已经找到了解决方案:答案 = 2x - 2。

函数f是一条简单的线,如图 8-1 所示。知道这一点后,你可以快速解出输入为 10 时的结果为 18。

显示线性方程

图 8-1。X = 10 意味着 Y = 18

从给定数据解决这个问题正是机器学习可以做到的。让我们准备并训练一个 TensorFlow.js 模型来解决这个简单的问题。

要应用监督学习,你需要做以下事情:

  1. 收集你的数据(输入和期望解决方案)。

  2. 创建和设计模型架构。

  3. 确定模型应该如何学习和衡量错误。

  4. 训练模型并确定训练时间。

数据准备

为了准备一台机器,你将编写代码来提供输入张量,即值[-1, 0, 1, 2, 3, 4]及其对应的答案[-4, -2, 0, 2, 4, 6]。问题的索引必须与预期答案的索引匹配,这在思考时是有意义的。因为我们给模型所有值的答案,这就是为什么这是一个监督学习问题。

在这种情况下,训练集有六个例子。机器学习很少会用在这么少的数据上,但问题相对较小且简单。正如你所看到的,没有任何训练数据被保留用于测试模型。幸运的是,你可以尝试这个模型,因为你知道最初用来创建数据的公式。如果你对训练和测试数据集的定义不熟悉,请查看第一章中的“常见 AI/ML 术语”。

设计模型

设计模型的想法可能听起来很繁琐,但诚实的答案是,这是理论、试验和错误的混合。在设计师了解架构的性能之前,模型可能需要经过数小时甚至数周的训练。整个研究领域可能致力于模型设计。您将为本书创建的 Layers 模型将为您提供良好的基础。

设计模型的最简单方法是使用 TensorFlow.js 的 Layers API,这是一个高级 API,允许您按顺序定义每个层。实际上,要启动您的模型,您将从代码tf.sequential();开始。您可能会听到这被称为“Keras API”,因为这种模型定义风格的起源。

您将创建的模型来解决您正在尝试解决的简单问题将只有一个层和一个神经元。当您考虑到一条线的公式时,这是有道理的;这不是一个非常复杂的方程。

注意

当你熟悉密集网络的基本方程时,就会惊讶地发现为什么在这种情况下单个神经元会起作用,因为一条线的公式是 y = mx + b,而人工神经元的公式是 y = Wx + b。

要向模型添加一层,您将使用model.add,然后定义您的层。使用 Layers API,每个添加的层都会自行定义并根据model.add调用的顺序自动连接,就像推送到数组一样。您将在第一层中定义模型的预期输入,并且您添加的最后一层将定义模型的输出(参见示例 8-1)。

示例 8-1。构建一个假设模型
model.add(ALayer)
model.add(BLayer)
model.add(CLayer)

// Currently, model is [ALayer, BLayer, CLayer]

示例 8-1 中的模型将有三层。ALayer将负责识别预期的模型输入和自身。BLayer不需要识别其输入,因为可以推断输入将是ALayer。因此,BLayer只需要定义自身。CLayer将识别自身,并且因为它是最后一个,这将确定模型的输出。

让我们回到您试图编码的模型。当前问题的架构模型目标只有一个具有一个神经元的层。当您编写该单个层时,您将定义您的输入和输出。

// The entire inner workings of the model
model.add(
  tf.layers.dense({
    inputShape: 1, // one value 1D tensor
    units: 1 // one neuron - output tensor
  })
);

结果是一个简单的神经网络。在绘制图时,网络有两个节点(参见图 8-2)。

2 nodes 1 edge nn

图 8-2。一个输入和一个输出

通常,层具有更多的人工神经元(图节点),但也更复杂,并具有其他要配置的属性。

识别学习指标

接下来,您需要告诉您的模型如何识别进展以及如何变得更好。这些概念并不陌生;它们在软件中只是看起来有点奇怪。

每当我试图将激光指示器对准某物时,我通常会错过。但是,我可以看到我稍微偏左或偏右,然后进行调整。机器学习也是如此。它可能会随机开始,但算法会自我纠正,并且需要知道您希望它如何做到这一点。最符合我的激光指示器示例的方法将是梯度下降。优化激光指示器的最平滑迭代方法称为随机梯度下降。这就是我们将在这种情况下使用的方法,因为它效果很好,并且对于您下次晚宴听起来相当酷。

至于测量错误,您可能认为简单的“对”和“错”会起作用,但是差几个小数点和错上千有很大的区别。因此,通常依赖损失函数来帮助您确定 AI 预测猜测的错误程度。有很多衡量错误的方法,但在这种情况下,均方误差(MSE)是一个很好的衡量标准。对于那些需要了解数学的人,MSE 是估计值(y)和实际值(带有小帽的 y)之间的平均平方差。如果您熟悉常见的数学符号,可以将其表示如下:

MSE = 1 n i=1 n (Y i -Y i ^) 2

为什么您喜欢这个公式而不是简单的原始答案距离?MSE 中蕴含了一些数学优势,有助于将方差和偏差作为正误差分数进行整合。不深入统计学,它是解决拟合数据线的最常见损失函数之一。

提示

随机梯度下降和均方误差散发着数学起源的气息,对于一个实用的开发人员来说,这些都无法告诉他们的目的。在这种情况下,最好吸收这些术语的含义,如果您感到有冒险精神,可以观看大量视频,详细解释它们。

当您准备告诉模型使用特定的学习指标并且添加完所有层到模型后,这一切都包含在 .compile 调用中。TensorFlow.js 足够聪明,了解梯度下降和均方误差。您可以通过它们的批准字符串等效项来识别它们,而不是编写这些函数:

model.compile({
  optimizer: "sgd",
  loss: "meanSquaredError"
});

使用框架的一个巨大好处是,随着机器学习世界发明新的优化器如“Adagrad”和“Adamax”,只需在模型架构中简单更改一个字符串¹,就可以尝试并调用它们。将“sgd”切换为“adamax”对于开发人员来说几乎不需要时间,可能会显著提高模型训练时间,而无需阅读有关随机优化的已发表论文。

在不了解函数的具体情况下识别函数,提供了一种苦乐参半的好处,类似于更改文件类型而无需了解每种类型的完整结构。对于每种的优缺点有一点了解是很有帮助的,但您不需要记住规范。在架构时花点时间阅读可用内容是值得的。

不用担心。您会看到相同的名称一遍又一遍地使用,所以很容易掌握它们。

此时,模型已创建。如果要求它预测任何内容,它将失败,因为它没有进行任何训练。架构中的权重完全是随机的,但您可以通过调用 model.summary() 来查看层。输出直接显示在控制台上,看起来有点像 示例 8-2。

示例 8-2. 在 Layers 模型上调用 model.summary() 打印层
_________________________________________________________________
Layer (type)                 Output shape              Param #
=================================================================
dense_Dense6 (Dense)         [null,1]                  2
=================================================================
Total params: 2
Trainable params: 2
Non-trainable params: 0
_________________________________________________________________

dense_Dense6是在 TensorFlow.js 后端中引用此层的自动 ID。您的 ID 可能会有所不同。这个模型有两个可训练参数,因为一条线是 y = mx + b,对吧?从视觉上看,这个模型的参数是可训练的。我们将在后面介绍不可训练的参数。这个单层模型已经准备就绪。

训练模型的任务

训练模型的最后一步是将输入组合到架构中,并指定训练的持续时间。如前所述,这通常以 epochs 来衡量,即模型将多少次查看带有正确答案的闪卡,然后完成后停止训练。您应该使用的 epochs 数量取决于问题的规模、模型以及“足够好”的正确性。在某些模型中,再获得半个百分点值得几小时的训练,而在我们的情况下,模型足够准确,可以在几秒内得到正确答案。

训练集是一个具有六个值的 1D 张量。如果 epochs 设置为 1,000,那么模型将有效地训练 6,000 次迭代,这将在任何现代计算机上最多花费几秒钟。将一条线拟合到点的微不足道的问题对于计算机来说非常简单。

将所有内容整合在一起

现在您已经熟悉了高级概念,您可能迫不及待地想用代码解决这个问题。以下是用数据训练模型的代码,然后立即要求模型为值10提供答案,如前所述。

// Inputs
const xs = tf.tensor([-1, 0, 1, 2, 3, 4]); // ①
// Answers we want from inputs
const ys = tf.tensor([-4, -2, 0, 2, 4, 6]);

// Create a model
const model = tf.sequential(); // ②

model.add( // ③
  tf.layers.dense({
    inputShape: 1,
    units: 1
  })
);

model.compile({ // ④
  optimizer: "sgd",
  loss: "meanSquaredError"
});

// Print out the model structure
model.summary();

// Train
model.fit(xs, ys, { epochs: 300 }).then(history => { // ⑤
  const inputTensor = tf.tensor([10]);
  const answer = model.predict(inputTensor); // ⑥
  console.log(`10 results in ${Math.round(answer.dataSync())}`);
  // cleanup
  tf.dispose([xs, ys, model, answer, inputTensor]); // ⑦
});

数据准备在具有输入和期望输出的张量中完成。

开始一个顺序模型。

添加唯一的具有一个输入和一个输出的层,如前所述。

使用给定的优化器和损失函数完成顺序模型。

模型被告知用fit进行 300 个 epochs 的训练。这是一个微不足道的时间量,当fit完成时,它返回的承诺会被解决。

要求训练模型为输入张量10提供答案。您需要四舍五入答案以获得整数结果。

在获得答案后立即处理所有内容。

恭喜!您已经用代码从头开始训练了一个模型。您刚刚解决的问题被称为线性回归问题。它有各种用途,是预测房价、消费者行为、销售预测等的常用工具。一般来说,在现实世界中,点并不完全落在一条直线上,但现在您有能力将分散的线性数据转化为预测模型。因此,当您的数据看起来像图 8-3 时,您可以像图 8-4 中所示解决问题。

图上的分散线性数据

图 8-3. 分散的线性数据

预测最佳拟合线的图表

图 8-4. 使用 TensorFlow.js 预测最佳拟合线

现在您已经熟悉了训练的基础知识,您可以扩展您的流程,了解解决更复杂模型所需的步骤。训练模型在很大程度上取决于架构以及数据的质量和数量。

非线性训练 101

如果每个问题都基于线性,那么就不需要机器学习了。统计学家从 19 世纪初就开始解决线性回归问题。不幸的是,一旦您的数据是非线性的,这种方法就会失败。如果您让 AI 解决 Y = X²会发生什么?

平方函数

图 8-5. 简单的 Y = X²

更复杂的问题需要更复杂的模型架构。在本节中,您将学习基于层的模型的新属性和特性,以及处理数据的非线性分组。

您可以向神经网络添加更多节点,但它们仍然存在于线性函数的分组中。为了打破线性,现在是时候添加激活函数了。

激活函数类似于大脑中的神经元。是的,这个比喻又来了。当一个神经元在电化学上接收到信号时,并不总是激活。在神经元发射动作电位之前,需要一个阈值。同样地,神经网络具有一定程度的偏见和类似于开关的动作电位,当它们达到由于传入信号而引起的阈值时发生(类似于去极化电流)。简而言之,激活函数使神经网络能够进行非线性预测。²

注意

如果您知道您的解决方案需要是二次的,那么有更聪明的方法来解决二次函数。在本节中,您将为 X²解决问题,这是专门为了更多地了解 TensorFlow.js 而编排的,而不是为了解决简单的数学函数。

是的,这个练习可以很容易地在不使用 AI 的情况下解决,但那样有什么乐趣呢?

收集数据

指数函数可能返回一些非常大的数字,加快模型训练速度的一个技巧是保持数字及其之间的距离较小。您会一次又一次地看到这一点。对于我们的目的,模型的训练数据将是 0 到 10 之间的数字。

const jsxs = [];
const jsys = [];

const dataSize = 10;
const stepSize = 0.001;
for (let i = 0; i < dataSize; i = i + stepSize) {
  jsxs.push(i);
  jsys.push(i * i);
}
// Inputs
const xs = tf.tensor(jsxs);
// Answers we want from inputs
const ys = tf.tensor(jsys);

这段代码准备了两个张量。xs张量是 10,000 个值的分组,ys是这些值的平方。

向神经元添加激活

为给定层中的神经元选择激活函数以及您的模型大小本身就是一门科学。这取决于您的目标、您的数据和您的知识。就像编码一样,您可以提出几种几乎同样有效的解决方案。经验和实践将帮助您找到适合的解决方案。

在添加激活时,重要的是要注意 TensorFlow.js 中内置了许多激活函数。其中最流行的激活函数之一被称为 ReLU,代表修正线性单元。正如您可能从名称中推断出的那样,它来自科学术语的核心,而不是机智的 NPM 软件包名称。有各种文献讨论了在某些模型中使用 ReLU 相对于其他激活函数的好处。您必须知道 ReLU 是激活函数的一个流行选择,开始使用它应该没问题。随着您对模型架构的了解越来越多,您应该随意尝试其他激活函数。与许多其他选择相比,ReLU 有助于模型更快地训练。

在上一个模型中,您只有一个节点和一个输出。现在增加网络的大小变得重要。没有一个固定的大小公式可供使用,因此每个问题的第一阶段通常需要一些试验。为了我们的目的,我们将增加一个包含 20 个神经元的密集层。密集层意味着该层中的每个节点都连接到其之前和之后的每个节点。生成的模型看起来像图 8-6。

当前神经网络形状和层

图 8-6. 神经网络架构(20 个神经元)

从左到右浏览图 8-6 中显示的架构,一个数字进入网络,20 个神经元的层被称为隐藏层,最终值输出在最后一层。隐藏层是输入和输出之间的层。这些隐藏层添加了可训练的神经元,并使模型能够处理更复杂的模式。

要添加这一层并为其提供激活函数,您将在序列中指定一个新的密集层:

model.add(
  tf.layers.dense({
    inputShape: 1, // ①
    units: 20, // ②
    activation: "relu" // ③
  })
);

model.add(
  tf.layers.dense({
    units: 1 // ④
  })
);

第一层将输入张量定义为一个单一数字。

指定层应该有 20 个节点。

为您的层指定一个花哨的激活函数。

添加最终的单单元层以获取输出值。

如果您编译模型并打印摘要,您将看到类似于示例 8-3 的输出。

示例 8-3。调用model.summary()以获取当前结构
_________________________________________________________________
Layer (type)                 Output shape              Param #
=================================================================
dense_Dense1 (Dense)         [null,20]                 40
_________________________________________________________________
dense_Dense2 (Dense)         [null,1]                  21
=================================================================
Total params: 61
Trainable params: 61
Non-trainable params: 0
_________________________________________________________________

此模型架构有两个与先前层创建代码匹配的层。null部分代表批量大小,由于它可以是任何数字,因此留空。例如,第一层表示为[null,20],因此四个值的批次将为模型提供输入[4, 20]

您会注意到模型共有 61 个可调参数。如果您查看图 8-6 中的图表,您可以绘制线条和节点以获取参数。第一层有 20 个节点和 20 条线连接到它们,这就是为什么它有 40 个参数。第二层有 20 条线都连接到一个单个节点,这就是为什么只有 21 个参数。您的模型已准备好训练,但这次要大得多。

如果您进行这些更改并开始训练,您可能会听到您的 CPU/GPU 风扇启动并看到一堆无用的东西。听起来计算机可能正在训练,但肯定很好看到某种进展。

观看训练

TensorFlow.js 拥有各种令人惊奇的工具,可帮助您识别训练进度。特别是,fit配置的一个属性称为callbacks。在callbacks对象内部,您可以连接到训练模型的某些生命周期,并运行任何您想要的代码。

由于您已经熟悉一个 epoch(对训练数据的完整运行),这是您在本例中将使用的时刻。这是一个简洁但有效的获取某种控制台消息的方法。

const printCallback = { // ①
  onEpochEnd: (epoch, log) => { // ②
    console.log(epoch, log); // ③
  }
};

创建包含您想要连接的所有生命周期方法的回调对象。

onEpochEnd是训练支持的许多已识别的生命周期回调之一。其他枚举在框架的fit部分文档中。

打印审查的值。通常,您会对这些信息进行更深入的处理。

注意

通过在fit配置中设置stepsPerEpoch数字,可以重新定义一个 epoch。使用此变量,一个 epoch 可以成为任何数量的训练数据。默认情况下,这设置为null,因此一个 epoch 设置为您的训练集中唯一样本的数量除以批量大小。

剩下要做的就是将您的对象传递给模型的fit配置,同时传递您的 epochs,您应该在模型训练时看到日志。

await model.fit(xs, ys, {
  epochs: 100,
  callbacks: printCallback
});

onEpochEnd回调会打印到您的控制台,显示训练正在进行。在图 8-7 中,您可以看到您的 epoch 和日志对象。

训练进度日志

图 8-7。epochs 19 到 26 的onEpochEnd日志

能够看到模型实际上正在训练,甚至能够知道它所处的 epoch,这真是一种清新的感觉。但是,日志值是怎么回事?

模型日志

模型通过损失函数告知如何定义损失。您希望在每个 epoch 中看到的是损失下降。损失不仅仅是“对还是错?”它是关于模型有多错,以便它可以学习。每个 epoch 之后,模型都会报告损失,在一个良好的模型架构中,这个数字会迅速下降。

您可能对查看准确性感兴趣。大多数情况下,准确性是一个很好的指标,我们可以在日志中启用准确性。但是,对于这样的模型,准确性并不是一个很好的指标。例如,如果您问模型预测[7]的输出应该是多少,而模型回答49.0676842而不是49,那么它的准确性为零,因为它是错误的。虽然接近的结果在四舍五入后会有较低的损失并且准确,但从技术上讲,它是错误的,模型的准确性评分会很差。让我们在它更有效时再启用准确性。

改进训练

损失值相当高。什么是高损失值?具体而言,这取决于问题。但是,当您看到错误值为 800+时,通常可以说训练尚未完成。

Adam 优化器

幸运的是,您不必让计算机训练几周。目前,优化器设置为随机梯度下降(sgd)的默认值。您可以修改sgd预设,甚至选择不同的优化器。最受欢迎的优化器之一称为 Adam。如果您有兴趣尝试 Adam,您不必阅读2015 年发表的 Adam 论文;您只需将sgd的值更改为adam,然后您就可以开始了。这是您可以享受框架优势的地方。只需更改一个小字符串,整个模型架构就已更改。Adam 对解决某些类型的问题具有显著的好处。

更新后的编译代码如下:

model.compile({
  optimizer: "adam",
  loss: "meanSquaredError"
});

使用新的优化器,损失在几个时期内降至 800 以下,甚至降至 1 以下,如您在图 8-8 中所见。

训练进度日志

图 8-8。时期 19 到 26 的onEpochEnd日志

经过 100 个时期,模型对我来说仍在取得进展,但在损失值为0.03833026438951492时停止。每次运行都会有所不同,但只要损失很小,模型就会正常工作。

提示

修改和调整模型架构以便为特定问题更快地训练或收敛是经验和实验的结合。

情况看起来不错,但还有一个功能我们应该添加,有时可以显著缩短训练时间。在一台相当不错的机器上,这 100 个时期大约需要 100 秒才能运行。您可以通过一行批处理数据来加快训练速度。当您将batchSize属性分配给fit配置时,训练速度会大大加快。尝试在 fit 调用中添加批处理大小:

await model.fit(xs, ys, {
  epochs: 100,
  callbacks: printCallback,
  batchSize: 64  // ①
});

对于我的机器,64 的batchSize将训练时间从 100 秒减少到 50 秒。

注意

批处理大小是效率和内存之间的权衡。如果批处理太大,这将限制能够运行训练的机器。

您有一个在合理时间内训练的模型,几乎没有额外的成本。但是,增加批处理大小是一个您可以并且应该审查的选项。

更多节点和层

整个时间内,模型的形状和大小一直是相同的:一个包含 20 个节点的“隐藏”层。不要忘记,您可以随时添加更多层。作为一个实验,添加另一个包含 20 个节点的层,这样您的模型架构看起来像图 8-9。

当前神经网络形状和层

图 8-9。神经网络架构(20×20 隐藏节点)

使用 Layers 模型架构,您可以通过添加一个新层来构建这个模型。请参阅以下代码:

model.add(
  tf.layers.dense({
    inputShape: 1,
    units: 20,
    activation: "relu"
  })
);

model.add(
  tf.layers.dense({
    units: 20,
    activation: "relu"
  })
);

model.add(
  tf.layers.dense({
    units: 1
  })
);

结果模型训练速度较慢,这是有道理的,但也收敛速度更快,这也是有道理的。这个更大的模型在 20 秒的训练时间内只需 30 个时期就为输入[7]生成了正确的值。

将所有内容放在一起,您的结果代码执行以下操作:

  • 创建一个重要的数据集

  • 创建几个深度连接的层,使用 ReLU 激活

  • 将模型设置为使用先进的 Adam 优化

  • 使用 64 块数据训练模型,并在途中打印进度

从头到尾的整个源代码如下:

const jsxs = [];((("improving training", "adding more neurons and layers")))
const jsys = [];

// Create the dataset
const dataSize = 10;
const stepSize = 0.001;
for (let i = 0; i < dataSize; i = i + stepSize) {
  jsxs.push(i);
  jsys.push(i * i);
}
// Inputs
const xs = tf.tensor(jsxs);
// Answers we want from inputs
const ys = tf.tensor(jsys);

// Print the progress on each epoch
const printCallback = {
  onEpochEnd: (epoch, log) => {
    console.log(epoch, log);
  }
};

// Create the model
const model = tf.sequential();
model.add(
  tf.layers.dense({
    inputShape: 1,
    units: 20,
    activation: "relu"
  })
);
model.add(
  tf.layers.dense({
    units: 20,
    activation: "relu"
  })
);
model.add(
  tf.layers.dense({
    units: 1
  })
);

// Compile for training
model.compile({
  optimizer: "adam",
  loss: "meanSquaredError"
});

// Train and print timing
console.time("Training");
await model.fit(xs, ys, {
  epochs: 30,
  callbacks: printCallback,
  batchSize: 64
});
console.timeEnd("Training");

// evaluate the model
const next = tf.tensor([7]);
const answer = model.predict(next);
answer.print();

// Cleanup!
answer.dispose();
xs.dispose();
ys.dispose();
model.dispose();

打印的结果张量与49非常接近。训练成功。虽然这是一次有点奇怪的冒险,但它突出了模型创建和验证过程的一部分。构建模型是你随着时间实验各种数据及其相关解决方案而获得的技能之一。

在接下来的章节中,你将解决更复杂但有益的问题,比如分类。你在这里学到的一切将成为你工作台上的一种工具。

章节回顾

你已经进入了训练模型的世界。层模型结构不仅是一个可理解的视觉,现在它是你可以理解和按需构建的东西。机器学习与普通软件开发非常不同,但你正在逐渐理解 TensorFlow.js 所提供的差异和好处。

章节挑战:模型架构师

现在轮到你通过规范构建一个 Layers 模型了。这个模型做什么?没人知道!它不会用任何数据进行训练。在这个挑战中,你将被要求构建一个具有各种你可能不理解的属性的模型,但你应该足够熟悉以至少设置好模型。这个模型将是你迄今为止创建的最大模型。你的模型将有五个输入和四个输出,它们之间有两层。它看起来像图 8-10。

你的章节挑战

图 8-10. 章节挑战模型

在你的模型中做以下操作:

  • 输入层应该有 5 个单元。

  • 下一层应该有 10 个单元并使用sigmoid激活。

  • 下一层应该有 7 个单元并使用 ReLU 激活。

  • 最后一层应该有 4 个单元并使用softmax激活。

  • 模型应该使用 Adam 优化。

  • 模型应该使用损失函数categoricalCrossentropy

在构建这个模型并查看摘要之前,你能计算出最终模型将有多少可训练参数吗?这是从图 8-10 中的总行数和圆圈数,不包括输入。

你可以在附录 B 中找到这个挑战的答案。

回顾问题

让我们回顾一下你在本章编写的代码中学到的经验。花点时间回答以下问题:

  1. 为什么章节挑战模型适用于本章的训练数据?

  2. 你可以调用哪个方法来记录和审查模型的结构?

  3. 为什么要向层添加激活函数?

  4. 你如何为 Layers 模型指定输入形状?

  5. sgd代表什么?

  6. 什么是一个 epoch?

  7. 如果一个模型有一个输入,然后是两个节点的一层,和两个节点的输出,那么有多少隐藏层?

这些练习的解决方案可以在附录 A 中找到。

¹ 支持的优化器列在 tfjs-core 的optimizers 文件夹中。

² 从 Andrew Ng 了解更多关于激活函数的知识。

第九章:分类模型和数据分析

“先见之明,后事之师。”

—Amelia Barr

你不仅仅是把数据丢进模型中是有原因的。神经网络以极快的速度运行并执行复杂的计算,就像人类可以瞬间做出反应一样。然而,对于人类和机器学习模型来说,反应很少包含合理的上下文。处理脏乱和令人困惑的数据会导致次优的模型,甚至什么都不会得到。在这一章中,你将探索识别、加载、清理和优化数据的过程,以提高 TensorFlow.js 模型的训练准确性。

我们将:

  • 确定如何创建分类模型

  • 学习如何处理 CSV 数据

  • 了解 Danfo.js 和 DataFrames

  • 确定如何将混乱的数据输入训练中(整理你的数据)

  • 练习绘制和分析数据

  • 了解机器学习笔记本

  • 揭示特征工程的核心概念

当你完成这一章时,你将有信心收集大量数据,分析数据,并通过使用上下文来创建有助于模型训练的特征来测试你的直觉。

在这一章中,你将构建一个Titanic生死分类器。30 岁的 Kate Connolly 小姐,持有三等舱票,会生还吗?让我们训练一个模型来获取这些信息,并给出生还的可能性。

分类模型

到目前为止,你训练了一个输出数字的模型。你消化的大多数模型与你创建的模型有些不同。在第八章中,你实现了线性回归,但在这一章中,你将实现一个分类模型(有时称为逻辑回归)。

毒性、MobileNet,甚至井字棋模型输出一种选择,从一组选项中选择。它们使用一组总和为一的数字,而不是一个没有范围的单个数字。这是分类模型的常见结构。一个旨在识别三种不同选项的模型将给出与每个选项对应的数字。

试图预测分类的模型需要一种将输出值映射到其相关类别的方法。到目前为止,在分类模型中,最常见的方法是输出它们的概率。要创建一个执行此操作的模型,你只需要在最终层实现特殊的激活函数:

提示

记住,激活函数帮助你的神经网络以非线性方式运行。每个激活函数使得一个层以所需的非线性方式运行,最终层的激活直接转化为输出。重要的是确保你学会了哪种激活函数会给你所需的模型输出。

在这本书中使用的模型中,你一遍又一遍看到的激活函数被称为softmax激活。这是一组值,它们的总和为一。例如,如果你的模型需要一个 True/False 输出,你会期望模型输出两个值,一个用于识别true的概率,另一个用于false。例如,对于这个模型,softmax 可能输出[0.66, 0.34],经过一些四舍五入。

这可以扩展到 N 个值的 N 个分类只要类别是互斥的。在设计模型时,你会在最终层强制使用 softmax,并且输出的数量将是你希望支持的类别数量。为了实现 True 或 False 的结果,你的模型架构将在最终层上使用 softmax 激活,有两个输出。

// Final layer softmax True/False
model.add(
  tf.layers.dense({
    units: 2,
    activation: "softmax"
  })
);

如果您试图从输入中检测几件事情会发生什么?例如,胸部 X 光可能同时对肺炎和肺气肿呈阳性。在这种情况下,Softmax 不起作用,因为输出必须总和为一,对一个的信心必须与另一个对抗。在这种情况下,有一种激活可以强制每个节点的值在零和一之间,因此您可以实现每个节点的概率。这种激活称为 sigmoid 激活。这可以扩展到 N 个值,用于 N 个不相互排斥的分类。这意味着您可以通过具有 sigmoid 的单个输出来实现真/假模型(二元分类),其中接近零为假,接近一为真:

// Final layer sigmoid True/False
model.add(
  tf.layers.dense({
    units: 1,
    activation: "sigmoid",
  })
);

是的,这些激活名称很奇怪,但它们并不复杂。您可以通过研究这些激活函数的工作原理背后的数学,在 YouTube 的兔子洞中轻松度过一天。但最重要的是,了解它们在分类中的用法。在 表 9-1 中,您将看到一些示例。

表 9-1. 二元分类示例

激活 输出 结果分析
sigmoid [0.999999] 99% 确定是真的
softmax [0.99, 0.01] 99% 确定是真的
sigmoid [0.100000] 10% 确定是真的(因此 90% 是假的)
softmax [0.10, 0.90] 90% 确定是假的

当您处理真/假时,您使用 softmaxsigmoid 的区别消失。在选择最终层的激活时,您选择哪种激活没有真正的区别,因为没有一种可以排除另一种。在本章中,我们将在最后一层使用 sigmoid 以简化。

如果您试图对多个事物进行分类,您需要在 sigmoidsoftmax 之间做出明智的选择。本书将重申和澄清这些激活函数的使用情况。

泰坦尼克号

1912 年 4 月 15 日,“不沉的” RMS 泰坦尼克号(请参见 图 9-1)沉没了。这场悲剧在历史书籍中广为流传,充满了傲慢的故事,甚至有一部由莱昂纳多·迪卡普里奥和凯特·温丝莱特主演的电影。这场悲剧充满了一丝令人敬畏的死亡好奇。如果您在拉斯维加斯卢克索的 泰坦尼克号 展览中,您的门票会分配给您一位乘客的名字,并告诉您您的票价、舱位等等关于您生活的几件事。当您浏览船只和住宿时,您可以通过您门票上的人的眼睛体验它。在展览结束时,您会发现您门票上印刷的人是否幸存下来。

泰坦尼克号概况

图 9-1. RMS 泰坦尼克号

谁生存谁没有是 100%随机的吗?熟悉历史或看过电影的人都知道这不是一个抛硬币的事情。也许您可以训练一个模型来发现数据中的模式。幸运的是,客人日志和幸存者名单可供我们使用。

泰坦尼克数据集

与大多数事物一样,数据现在已转录为数字格式。 泰坦尼克 名单以逗号分隔的值(CSV)形式可用。这种表格数据可以被任何电子表格软件读取。有很多副本的 泰坦尼克 数据集可用,并且它们通常具有相同的信息。我们将使用的 CSV 文件可以在本章的相关代码中的 额外文件夹 中找到。

这个 泰坦尼克 数据集包含在 表 9-2 中显示的列数据。

表 9-2. 泰坦尼克数据

定义 图例
生存 生存 0 = 否,1 = 是
pclass 票类 1 = 1 等,2 = 2 等,3 = 3 等
性别 性别
年龄 年龄
兄弟姐妹或配偶数量 兄弟姐妹或配偶在船上的数量
父母或子女数量 父母或子女在船上的数量
票号 票号
票价 乘客票价
船舱 船舱号码
embarked 登船港口 C = 瑟堡, Q = 昆士敦, S = 南安普敦

那么如何将这些 CSV 数据转换为张量形式呢?一种方法是读取 CSV 文件,并将每个输入转换为张量表示进行训练。当您试图尝试哪些列和格式对训练模型最有用时,这听起来是一个相当重要的任务。

在 Python 社区中,一种流行的加载、修改和训练数据的方法是使用一个名为Pandas的库。这个开源库在数据分析中很常见。虽然这对 Python 开发人员非常有用,但 JavaScript 中存在类似工具的需求很大。

Danfo.js

Danfo.js是 Pandas 的 JavaScript 开源替代品。Danfo.js 的 API 被故意保持与 Pandas 接近,以便利用信息体验共享。甚至 Danfo.js 中的函数名称都是snake_case而不是标准的 JavaScriptcamelCase格式。这意味着您可以在 Danfo.js 中最小地进行翻译,利用 Pandas 的多年教程。

我们将使用 Danfo.js 来读取Titanic CSV 并将其修改为 TensorFlow.js 张量。要开始,您需要将 Danfo.js 添加到项目中。

要安装 Danfo.js 的 Node 版本,您将运行以下命令:

$ npm i danfojs-node

如果您使用简单的 Node.js,则可以require Danfo.js,或者如果您已经配置了代码以使用 ES6+,则可以import

const dfd = require("danfojs-node");
注意

Danfo.js 也可以在浏览器中运行。本章依赖于比平常更多的打印信息,因此利用完整的终端窗口并依赖 Node.js 的简单性来访问本地文件是有意义的。

Danfo.js 在幕后由 TensorFlow.js 提供支持,但它提供了常见的数据读取和处理实用程序。

为泰坦尼克号做准备

机器学习最常见的批评之一是它看起来像一个金鹅。您可能认为接下来的步骤是将模型连接到 CSV 文件,点击“训练”,然后休息一天,去公园散步。尽管每天都在努力改进机器学习的自动化,但数据很少以“准备就绪”的格式存在。

本章中的Titanic数据包含诱人的 Train 和 Test CSV 文件。然而,使用 Danfo.js,我们很快就会看到提供的数据远未准备好加载到张量中。本章的目标是让您识别这种形式的数据并做好适当的准备。

读取 CSV

CSV 文件被加载到一个称为 DataFrame 的结构中。DataFrame 类似于带有可能不同类型列和适合这些类型的行的电子表格,就像一系列对象。

DataFrame 有能力将其内容打印到控制台,以及许多其他辅助函数以编程方式查看和编辑内容。

让我们回顾一下以下代码,它将 CSV 文件读入 DataFrame,然后在控制台上打印几行:

const df = await dfd.read_csv("file://../../extra/titanic data/train.csv");  // ①
df.head().print(); // ②

read_csv方法可以从 URL 或本地文件 URI 中读取。

DataFrame 可以限制为前五行,然后打印。

正在加载的 CSV 是训练数据,print()命令将 DataFrame 的内容记录到控制台。结果显示在控制台中,如图 9-2 所示。

Head printout

图 9-2。打印 CSV DataFrame 头

在检查数据内容时,您可能会注意到一些奇怪的条目,特别是在Cabin列中,显示为NaN。这些代表数据集中的缺失数据。这是您不能直接将 CSV 连接到模型的原因之一:重要的是要确定如何处理缺失信息。我们将很快评估这个问题。

Danfo.js 和 Pandas 有许多有用的命令,可以帮助您熟悉加载的数据。一个流行的方法是调用.describe(),它试图分析每列的内容作为报告:

// Print the describe data
df.describe().print();

如果打印 DataFrame 的describe数据,您将看到您加载的 CSV 有 891 个条目,以及它们的最大值、最小值、中位数等的打印输出,以便您验证信息。打印的表格看起来像图 9-3。

描述打印输出

图 9-3。描述 DataFrame

一些列已从图 9-3 中删除,因为它们包含非数字数据。这是您将在 Danfo.js 中轻松解决的问题。

调查 CSV

这个 CSV 反映了数据的真实世界,其中经常会有缺失信息。在训练之前,您需要处理这个问题。

您可以使用isna()找到所有缺失字段,它将为每个缺失字段返回truefalse。然后,您可以对这些值进行求和或计数以获得结果。以下是将报告数据集的空单元格或属性的代码:

// Count of empty spots
empty_spots = df.isna().sum();
empty_spots.print();
// Find the average
empty_rate = empty_spots.div(df.isna().count());
empty_rate.print();

通过结果,您可以看到以下内容:

  • 空的Age数值:177(20%)

  • 空的Cabin数值:687(77%)

  • 空的Embarked数值:2(0.002%)

从对缺失数据量的简短查看中,您可以看到您无法避免清理这些数据。解决缺失值问题将至关重要,删除像PassengerId这样的无用列,并最终对您想保留的非数字列进行编码。

为了不必重复操作,您可以将 CSV 文件合并、清理,然后创建两个准备好用于训练和测试的新 CSV 文件。

目前,这些是步骤:

  1. 合并 CSV 文件。

  2. 清理 DataFrame。

  3. 从 DataFrame 重新创建 CSV 文件。

合并 CSV

要合并 CSV 文件,您将创建两个 DataFrame,然后沿着轴连接它们,就像对张量一样。您可能会感觉到张量训练引导您管理和清理数据的路径,并且这并非偶然。尽管术语可能略有不同,但您从前几章积累的概念和直觉将对您有所帮助。

// Load the training CSV
const df = await dfd.read_csv("file://../../extra/titanic data/train.csv");
console.log("Train Size", df.shape[0]) // ①

// Load the test CSV
const dft = await dfd.read_csv("file://../../extra/titanic data/test.csv");
console.log("Test Size", dft.shape[0]) // ②

const mega = dfd.concat({df_list: [df, dft], axis: 0})
mega.describe().print() // ③

打印“训练集大小为 891”

打印“测试集大小为 418”

显示一个包含 1,309 的表

使用熟悉的语法,您已经加载了两个 CSV 文件,并将它们合并成一个名为mega的 DataFrame,现在您可以对其进行清理。

清理 CSV

在这里,您将处理空白并确定哪些数据实际上是有用的。您需要执行三个操作来正确准备用于训练的 CSV 数据:

  1. 修剪特征。

  2. 处理空白。

  3. 迁移到数字。

修剪特征意味着删除对结果影响很小或没有影响的特征。为此,您可以尝试实验、绘制数据图表,或者简单地运用您的个人直觉。要修剪特征,您可以使用 DataFrame 的.drop函数。.drop函数可以从 DataFrame 中删除整个列或指定的行。

对于这个数据集,我们将删除对结果影响较小的列,例如乘客的姓名、ID、票和舱位。您可能会认为其中许多特征可能非常重要,您是对的。但是,我们将让您在本书之外的范围内研究这些特征。

// Remove feature columns that seem less useful
const clean = mega.drop({
  columns: ["Name", "PassengerId", "Ticket", "Cabin"],
});

要处理空白,您可以填充或删除行。填充空行是一种称为插补的技术。虽然这是一个很好的技能可以深入研究,但它可能会变得复杂。在本章中,我们将采取简单的方法,仅删除任何具有缺失值的行。要删除任何具有空数据的行,我们可以使用dropna()函数。

警告

这是在删除列之后之后完成的至关重要。否则,Cabin列中 77%的缺失数据将破坏数据集。

您可以使用以下代码删除所有空行:

// Remove all rows that have empty spots
const onlyFull = clean.dropna();
console.log(`After mega-clean the row-count is now ${onlyFull.shape[0]}`);

此代码的结果将数据集从 1,309 行减少到 1,043 行。将其视为一种懒惰的实验。

最后,您剩下两列是字符串而不是数字(EmbarkedSex)。这些需要转换为数字。

Embarked的值,供参考,分别是:C = 瑟堡,Q = 昆士敦,S = 南安普敦。有几种方法可以对其进行编码。一种方法是用数字等价物对其进行编码。Danfo.js 有一个LabelEncoder,它可以读取整个列,然后将值转换为数字编码的等价物。LabelEncoder将标签编码为介于0n-1之间的值。要对Embarked列进行编码,您可以使用以下代码:

// Handle embarked characters - convert to numbers
const encode = new dfd.LabelEncoder(); // ①
encode.fit(onlyFull["Embarked"]); // ②
onlyFull["Embarked"] = encode.transform(onlyFull["Embarked"].values); // ③
onlyFull.head().print(); // ④

创建一个新的LabelEncoder实例。

适合对Embarked列的内容进行编码的实例。

将列转换为值,然后立即用生成的列覆盖当前列。

打印前五行以验证替换是否发生。

您可能会对像第 3 步那样覆盖 DataFrame 列的能力感到惊讶。这是处理 DataFrame 而不是张量的许多好处之一,尽管 TensorFlow.js 张量在幕后支持 Danfo.js。

现在您可以使用相同的技巧对male / female字符串进行编码。(请注意,出于模型目的和乘客名单中可用数据的考虑,我们将性别简化为二进制。)完成后,您的整个数据集现在是数字的。如果在 DataFrame 上调用describe,它将呈现所有列,而不仅仅是几列。

保存新的 CSV 文件

现在您已经创建了一个可用于训练的数据集,您需要返回两个 CSV 文件,这两个文件进行了友好的测试和训练拆分。

您可以使用 Danfo.js 的.sample重新拆分 DataFrame。.sample方法会从 DataFrame 中随机选择 N 行。从那里,您可以将剩余的未选择值创建为测试集。要删除已抽样的值,您可以按索引而不是整个列删除行。

DataFrame 对象具有to_csv转换器,可选择性地接受要写入的文件参数。to_csv命令会写入参数文件并返回一个 promise,该 promise 解析为 CSV 内容。重新拆分 DataFrame 并写入两个文件的整个代码可能如下所示:

// 800 random to training
const newTrain = onlyFull.sample(800)
console.log(`newTrain row count: ${newTrain.shape[0]}`)
// The rest to testing (drop via row index)
const newTest = onlyFull.drop({index: newTrain.index, axis: 0})
console.log(`newTest row count: ${newTest.shape[0]}`)

// Write the CSV files
await newTrain.to_csv('../../extra/cleaned/newTrain.csv')
await newTest.to_csv('../../extra/cleaned/newTest.csv')
console.log('Files written!')

现在您有两个文件,一个包含 800 行,另一个包含 243 行用于测试。

泰坦尼克号数据的训练

在对数据进行训练之前,您需要处理最后一步,即经典的机器学习标记输入和预期输出(X 和 Y,分别)。这意味着您需要将答案(Survived列)与其他输入分开。为此,您可以使用iloc声明要创建新 DataFrame 的列的索引。

由于第一列是Survived列,您将使 X 跳过该列并抓取其余所有列。您将从 DataFrame 的索引一到末尾进行识别。这写作1:。您可以写1:9,这将抓取相同的集合,但1:表示“从索引零之后的所有内容”。iloc索引格式表示您为 DataFrame 子集选择的范围。

Y 值,或答案,是通过抓取Survived列来选择的。由于这是单列,无需使用iloc不要忘记对测试数据集执行相同操作

机器学习模型期望张量,而由于 Danfo.js 建立在 TensorFlow.js 上,将 DataFrame 转换为张量非常简单。最终,您可以通过访问.tensor属性将 DataFrame 转换为张量。

// Get cleaned data
const df = await dfd.read_csv("file://../../extra/cleaned/newTrain.csv");
console.log("Train Size", df.shape[0]);
const dft = await dfd.read_csv("file://../../extra/cleaned/newTest.csv");
console.log("Test Size", dft.shape[0]);

// Split train into X/Y
const trainX = df.iloc({ columns: [`1:`] }).tensor;
const trainY = df["Survived"].tensor;

// Split test into X/Y
const testX = dft.iloc({ columns: [`1:`] }).tensor;
const testY = dft["Survived"].tensor;

这些值已准备好被馈送到一个用于训练的模型中。

我在这个问题上使用的模型经过很少的研究后是一个具有三个隐藏层和一个具有 Sigmoid 激活的输出张量的序列层模型。

模型的组成如下:

model.add(
  tf.layers.dense({
    inputShape,
    units: 120,
    activation: "relu", // ①
    kernelInitializer: "heNormal", // ②
  })
);
model.add(tf.layers.dense({ units: 64, activation: "relu" }));
model.add(tf.layers.dense({ units: 32, activation: "relu" }));
model.add(
  tf.layers.dense({
    units: 1,
    activation: "sigmoid", // ③
  })
);

model.compile({
  optimizer: "adam",
  loss: "binaryCrossentropy", // ④
  metrics: ["accuracy"],      // ⑤
});

每一层都使用 ReLU 激活,直到最后一层。

这一行告诉模型根据算法初始化权重,而不是简单地将模型的初始权重设置为完全随机。这有时可以帮助模型更接近答案。在这种情况下并不是关键,但这是 TensorFlow.js 的一个有用功能。

最后一层使用 Sigmoid 激活来打印一个介于零和一之间的数字(生存或未生存)。

在训练二元分类器时,最好使用一个与二元分类一起工作的花哨命名的函数来评估损失。

这显示了日志中的准确性,而不仅仅是损失。

当您将模型fit到数据时,您可以识别测试数据,并获得模型以前从未见过的数据的结果。这有助于防止过拟合:

await model.fit(trainX, trainY, {
  batchSize: 32,
  epochs: 100,
  validationData: [testX, testY] // ①
})

提供模型应该在每个 epoch 上验证的数据。

注意

在前面的fit方法中显示的训练配置没有利用回调。如果您在tfjs-node上训练,您将自动看到训练结果打印到控制台。如果您使用tfjs,您需要添加一个onEpochEnd回调来打印训练和验证准确性。这两者的示例都在相关的本章源代码中提供。

在训练了 100 个 epoch 后,这个模型在训练数据上的准确率为 83%,在测试集的验证上也是 83%。从技术上讲,每次训练的结果会有所不同,但它们应该几乎相同:acc=0.827 loss=0.404 val_acc=0.831 val_loss=0.406

该模型已经识别出一些模式,并击败了纯粹的机会(50%准确率)。很多人在这里停下来庆祝创造一个几乎没有努力就能工作 83%的模型。然而,这也是一个很好的机会来认识 Danfo.js 和特征工程的好处。

特征工程

如果你在互联网上浏览一下,80%是Titanic数据集的一个常见准确率分数。我们已经超过了这个分数,而且没有真正的努力。然而,仍然有改进模型的空间,这直接来源于改进数据。

抛出空白数据是一个好选择吗?存在可以更好强调的相关性吗?模式是否被正确组织为模型?您能预先处理和组织数据得越好,模型就越能找到和强调模式。许多机器学习的突破都来自于在将模式传递给神经网络之前简化模式的技术。

这是“只是倾倒数据”停滞不前的地方,特征工程开始发展。Danfo.js 让您通过分析模式和强调关键特征来提升您的特征。您可以在交互式的 Node.js 读取求值打印循环(REPL)中进行这项工作,或者甚至可以利用为评估和反馈循环构建的网页。

让我们尝试通过确定并向数据添加特征来提高上述模型的准确率至 83%以上,使用一个名为 Dnotebook 的 Danfo.js Notebook。

Dnotebook

Danfo 笔记本,或Dnotebook,是一个交互式网页,用于使用 Danfo.js 实验、原型设计和定制数据。Python 的等价物称为 Jupyter 笔记本。您可以通过这个笔记本实现的数据科学将极大地帮助您的模型。

我们将使用 Dnotebook 来创建和共享实时代码,以及利用内置的图表功能来查找泰坦尼克号数据集中的关键特征和相关性。

通过创建全局命令来安装 Dnotebook:

$ npm install -g dnotebook

当您运行$ dnotebook时,将自动运行本地服务器并打开一个页面到本地笔记本站点,它看起来有点像图 9-4。

Dnotebook 新鲜截图

图 9-4。正在运行的新鲜 Dnotebook

每个 Dnotebook 单元格可以是代码或文本。文本采用 Markdown 格式。代码可以打印输出,并且未使用constlet初始化的变量可以在单元格之间保留。请参见图 9-5 中的示例。

Dnotebook 演示截图

图 9-5。使用 Dnotebook 单元格

图 9-5 中的笔记本可以从本章的extra/dnotebooks文件夹中的explaining_vars.json文件中下载并加载。这使得它适合用于实验、保存和共享。

泰坦尼克号视觉

如果您可以在数据中找到相关性,您可以将其作为训练数据中的附加特征强调,并在理想情况下提高模型的准确性。使用 Dnotebook,您可以可视化数据并在途中添加评论。这是分析数据集的绝佳资源。我们将加载两个 CSV 文件并将它们组合,然后直接在笔记本中打印结果。

您可以创建自己的笔记本,或者可以从相关源代码加载显示的笔记本的 JSON。只要您能够跟上图 9-6 中显示的内容,任何方法都可以。

指导性代码截图

图 9-6。加载 CSV 并在 Dnotebook 中组合它们

load_csv命令类似于read_csv命令,但在加载 CSV 内容时在网页上显示友好的加载动画。您可能还注意到了table命令的使用。table命令类似于 DataFrame 的print(),只是它为笔记本生成了输出的 HTML 表格,就像您在图 9-6 中看到的那样。

现在您已经有了数据,让我们寻找可以强调的重要区别,以供我们的模型使用。在电影《泰坦尼克号》中,当装载救生艇时他们大声喊着“妇女和儿童优先”。那真的发生了吗?一个想法是检查男性与女性的幸存率。您可以通过使用groupby来做到这一点。然后您可以打印每个组的平均值。

grp = mega_df.groupby(['Sex'])
table(grp.col(['Survived']).mean())

而且哇啊!您可以看到 83%的女性幸存下来,而只有 14%的男性幸存下来,正如图 9-7 中所示。

幸存率截图

图 9-7。女性更有可能幸存

您可能会想知道也许只是因为泰坦尼克号上有更多女性,这就解释了倾斜的结果,所以您可以快速使用count()来检查,而不是像刚才那样使用mean()

survival_count = grp.col(['Survived']).count()
table(survival_count)

通过打印的结果,您可以看到尽管幸存比例偏向女性,但幸存的男性要多得多。这意味着性别是幸存机会的一个很好的指标,因此应该强调这一特征。

使用 Dnotebook 的真正优势在于它利用了 Danfo.js 图表。例如,如果我们想看到幸存者的直方图,而不是分组用户,您可以查询所有幸存者,然后绘制结果。

要查询幸存者,您可以使用 DataFrame 的 query 方法:

survivors = mega_df.query({column: "Survived", is: "==", to: 1 })

然后,要在 Dnotebooks 中打印图表,您可以使用内置的viz命令,该命令需要一个 ID 和回调函数,用于填充笔记本中生成的 DIV。

直方图可以使用以下方式创建:

viz(`agehist`, x => survivors["Age"].plot(x).hist())

然后笔记本将显示生成的图表,如图 9-8 所示。

存活直方图的屏幕截图

图 9-8. 幸存者年龄直方图

在这里,您可以看到儿童的显着存活率高于老年人。再次,确定每个年龄组的数量和百分比可能值得,但似乎特定的年龄组或区间比其他年龄组表现更好。这给了我们可能改进模型的第二种方法。

让我们利用我们现在拥有的信息,再次尝试打破 83%准确率的记录。

创建特征(又称预处理)

在成长过程中,我被告知激活的神经元越多,记忆就会越强烈,因此请记住气味、颜色和事实。让我们看看神经网络是否也是如此。我们将乘客性别移动到两个输入,并创建一个经常称为分桶分箱的年龄分组。

我们要做的第一件事是将性别从一列移动到两列。这通常称为独热编码。目前,Sex具有数字编码。乘客性别的独热编码版本将0转换为[1, 0],将1转换为[0, 1],成功地将值移动到两列/单元。转换后,您删除Sex列并插入两列,看起来像图 9-9。

Danfo One-Hot Coded

图 9-9. 描述性别独热编码

要进行独热编码,Danfo.js 和 Pandas 都有一个get_dummies方法,可以将一列转换为多个列,其中只有一个列的值为 1。在 TensorFlow.js 中,进行独热编码的方法称为oneHot,但在 Danfo.js 中,get_dummies是向二进制变量致敬的方法,统计学中通常称为虚拟变量。编码结果后,您可以使用dropaddColumn进行切换:

// Handle person sex - convert to one-hot
const sexOneHot = dfd.get_dummies(mega['Sex']) // ①
sexOneHot.head().print()
// Swap one column for two
mega.drop({ columns: ['Sex'], axis: 1, inplace: true }) // ②
mega.addColumn({ column: 'male', value: sexOneHot['0'] }) // ③
mega.addColumn({ column: 'female', value: sexOneHot['1'] })

使用get_dummies对列进行编码

Sex列上使用inplace删除

添加新列,将标题切换为男性/女性

接下来,您可以使用apply方法为年龄创建桶。apply方法允许您在整个列上运行条件代码。根据我们的需求,我们将定义一个在我们的图表中看到的重要年龄组的函数,如下所示:

// Group children, young, and over 40yrs
function ageToBucket(x) {
  if (x < 10) {
    return 0
  } else if (x < 40) {
    return 1
  } else {
    return 2
  }
}

然后,您可以使用您定义的ageToBucket函数创建并添加一个完全新的列来存储这些桶:

// Create Age buckets
ageBuckets = mega['Age'].apply(ageToBucket)
mega.addColumn({ column: 'Age_bucket', value: ageBuckets })

这添加了一个值范围从零到二的整列。

最后,我们可以将我们的数据归一化为介于零和一之间的数字。缩放值会使值之间的差异归一化,以便模型可以识别模式和缩放原始数字中扭曲的差异。

注意

将归一化视为一种特征。如果您正在处理来自各个国家的 10 种不同货币,可能会感到困惑。归一化会缩放输入,使它们具有相对影响的大小。

const scaler = new dfd.MinMaxScaler()
scaledData = scaler.fit(featuredData)
scaledData.head().print()

从这里,您可以为训练编写两个 CSV 文件并开始!另一个选项是您可以编写一个单独的 CSV 文件,而不是使用特定的 X 和 Y 值设置validationData,您可以设置一个名为validationSplit的属性,该属性将为验证数据拆分出一定比例的数据。这样可以节省我们一些时间和麻烦,所以让我们使用validationSplit来训练模型,而不是显式传递validationData

生成的fit如下所示:

await model.fit(trainX, trainY, {
  batchSize: 32,
  epochs: 100,
  // Keep random 20% for validation on the fly.
  // The 20% is selected at the beginning of the training session.
  validationSplit: 0.2,
})

模型使用新数据进行 100 个时代的训练,如果您使用tfjs-node,即使没有定义回调函数,也可以看到打印的结果。

特征工程训练结果

上次,模型准确率约为 83%。现在,使用相同的模型结构但添加了一些特征,我们达到了 87%的训练准确率和 87%的验证准确率。具体来说,我的结果是acc=0.867 loss=0.304 val_acc=0.871 val_loss=0.370

准确性提高了,损失值低于以前。真正了不起的是,准确性和验证准确性都是对齐的,因此模型不太可能过拟合。这通常是神经网络在泰坦尼克号数据集中的较好得分之一。对于这样一个奇怪的问题,创建一个相当准确的模型已经达到了解释如何从数据中提取有用信息的目的。

审查结果

解决泰坦尼克号问题以达到 87%的准确率需要一些技巧。您可能仍然在想结果是否可以改进,答案肯定是“是”,因为其他人已经在排行榜上发布了更令人印象深刻的分数。在没有排行榜的情况下,评估是否有增长空间的常见方法是与一个受过教育的人在面对相同问题时的得分进行比较。

如果您是一个高分狂热者,章节挑战将有助于改进我们已经创建的令人印象深刻的模型。一定要练习工程特征,而不是过度训练,从而使模型过度拟合以基本上记住答案。

查找重要值、归一化特征和强调显著相关性是机器学习训练中的一项有用技能,现在您可以使用 Danfo.js 来实现这一点。

章节回顾

那么在本章开始时我们识别的那个个体发生了什么?凯特·康诺利小姐,一个 30 岁的持有三等舱票的女人,确实幸存了泰坦尼克号事故,模型也认同。

我们是否错过了一些提高机器学习模型准确性的史诗机会?也许我们应该用-1填充空值而不是删除它们?也许我们应该研究一下泰坦尼克号的船舱结构?或者我们应该查看parchsibsppclass,为独自旅行的三等舱乘客创建一个新列?“我永远不会放手!”

并非所有数据都可以像泰坦尼克号数据集那样被清理和特征化,但这对于机器学习来说是一次有用的数据科学冒险。有很多 CSV 文件可用,自信地加载、理解和处理它们对于构建新颖模型至关重要。像 Danfo.js 这样的工具使您能够处理这些海量数据,现在您可以将其添加到您的机器学习工具箱中。

注意

如果您已经是其他 JavaScript 笔记本的粉丝,比如ObservableHQ.com,Danfo.js 也可以导入并与这些笔记本轻松集成。

处理数据是一件复杂的事情。有些问题更加明确,根本不需要对特征进行任何调整。如果您感兴趣,可以看看像帕尔默企鹅这样的更简单的数据集。这些企鹅根据它们的嘴的形状和大小明显地区分为不同的物种。另一个简单的胜利是第七章中提到的鸢尾花数据集。

章节挑战:船只发生了什么

您知道在泰坦尼克号沉没中没有一个牧师幸存下来吗?像先生、夫人、小姐、牧师等这样的头衔桶/箱可能对模型的学习有用。这些敬称——是的,就是它们被称为的——可以从被丢弃的Name列中收集和分析。

在这个章节挑战中,使用 Danfo.js 识别在泰坦尼克号上使用的敬称及其相关的生存率。这是一个让您熟悉 Dnotebooks 的绝佳机会。

您可以在附录 B 中找到这个挑战的答案。

审查问题

让我们回顾一下你在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. 对于一个石头-剪刀-布分类器,你会使用什么样的激活函数?

  2. 在一个 sigmoid“狗还是不是狗”模型的最终层中会放置多少个节点?

  3. 加载一个具有内置 Danfo.js 的交互式本地托管笔记本的命令是什么?

  4. 如何将具有相同列的两个 CSV 文件的数据合并?

  5. 你会使用什么命令将单个列进行独热编码成多个列?

  6. 你可以使用什么来将 DataFrame 的所有值在 0 和 1 之间进行缩放?

这些练习的解决方案可以在附录 A 中找到。

第十章:图像训练

“一切都应该尽可能简单。但不要过于简单。”

—阿尔伯特·爱因斯坦

我将向您描述一个数字。我希望您根据其特征的描述来猜出这个数字。我想的数字顶部是圆的,右侧只有一条线,底部有一个重叠的环状物。花点时间,心理上映射我刚刚描述的数字。有了这三个特征,你可能可以猜出来。

视觉数字的特征可能会有所不同,但聪明的描述意味着您可以在脑海中识别数字。当我说“圆顶”时,您可能会立即排除一些数字,同样的情况也适用于“只有右侧有一条线”。这些特征组成了数字的独特之处。

如果您按照图 10-1 中显示的数字描述,并将其描述放入 CSV 文件中,您可能可以通过训练好的神经网络在这些数据上获得 100%的准确性。整个过程都很顺利,只是依赖于人类描述每个数字的顶部、中部和底部。如何自动化这个人类方面呢?如果您可以让计算机识别图像的独特特征,如环、颜色和曲线,然后将其输入神经网络,机器就可以学习将描述分类为图像所需的模式。

数字二的三部分。

图 10-1。如果您发现这是数字二,恭喜您。

幸运的是,通过出色的计算机视觉技巧,解决了图像特征工程的问题。

我们将:

  • 学习模型的卷积层的概念

  • 构建您的第一个多类模型

  • 学习如何读取和处理图像以供 TensorFlow.js 使用

  • 在画布上绘制并相应地对绘图进行分类

完成本章后,您将能够创建自己的图像分类模型。

理解卷积

卷积来自于数学世界中表达形状和函数的概念。您可以深入研究卷积在数学中的概念,然后从头开始将这些知识重新应用到数字图像信息的收集概念。如果您是数学和计算机图形的爱好者,这是一个非常令人兴奋的领域。

然而,当你有像 TensorFlow.js 这样的框架时,花费一周学习卷积操作的基础并不是必要的。因此,我们将专注于卷积操作的高级优势和特性,以及它们在神经网络中的应用。始终鼓励您在这个快速入门之外深入研究卷积的深层历史。

让我们看看您应该从非数学解释的卷积中获得的最重要概念。

图 10-2 中的两个数字二的图像是完全相同的数字,只是它们在边界框中从左到右移动。将这两个数字转换为张量后,会创建出两个明显不同的不相等张量。然而,在本章开头描述的特征系统中,这些特征将是相同的。

卷积特征与无特征

图 10-2。卷积简化了图像的本质

对于视觉张量,图像的特征比每个像素的确切位置更重要。

卷积快速总结

卷积操作用于提取高级特征,如边缘、梯度、颜色等。这些是分类给定视觉的关键特征。

那么应该提取哪些特征呢?这并不是我们实际决定的。您可以控制在查找特征时使用的滤波器数量,但最好定义可用模式的实际特征是在训练过程中定义的。这些滤波器从图像中突出和提取特征。

例如,看一下图 10-3 中的照片。南瓜灯是多种颜色,几乎与模糊但略带明暗的背景形成对比。作为人类,您可以轻松识别出照片中的内容。

南瓜灯艺术的照片

图 10-3. 南瓜灯艺术

现在这是同一图像,通过 3 x 3 的边缘检测滤波器卷积在像素上。注意结果在图 10-4 中明显简化和更明显。

不同的滤波器突出显示图像的不同方面,以简化和澄清内容。不必止步于此;您可以运行激活以强调检测到的特征,甚至可以在卷积上运行卷积。

结果是什么?您已经对图像进行了特征工程。通过各种滤波器对图像进行预处理,让您的神经网络看到原本需要更大、更慢和更复杂模型才能看到的模式。

前一图像的卷积结果

图 10-4. 卷积结果

添加卷积层

感谢 TensorFlow.js,添加卷积层与添加密集层一样简单,但称为conv2d,并具有自己的属性。

tf.layers.conv2d({
  filters: 32, // ①
  kernelSize: 3, // ②
  strides: 1, // ③
  padding: 'same', // ④
  activation: 'relu',  // ⑤
  inputShape: [28, 28, 1] // ⑥
})

确定要运行多少个滤波器。

kernelSize控制滤波器的大小。这里的3表示 3 x 3 的滤波器。

小小的 3 x 3 滤波器不适合您的图像,因此需要在图像上滑动。步幅是滤波器每次滑动的像素数。

填充允许卷积在strideskernelSize不能均匀地分割成图像宽度和高度时决定如何处理。当将填充设置为same时,会在图像周围添加零,以保持生成的卷积图像的大小不变。

然后将结果通过您选择的激活函数运行。

输入是一个图像张量,因此输入图像是模型的三维形状。这不是卷积的必需限制,正如您在第六章中学到的那样,但如果您不是在制作完全卷积模型,则建议这样做。

不要被可能的参数列表所压倒。想象一下自己必须编写所有这些不同设置。您可以像现有模型那样配置您的卷积,也可以使用数字进行调整以查看其对结果的影响。调整这些参数并进行实验是 TensorFlow.js 等框架的好处。最重要的是,随着时间的推移建立您的直觉。

重要的是要注意,这个conv2d层是用于图像的。同样,您将在线性序列上使用conv1d,在处理 3D 空间对象时使用conv3d。大多数情况下,使用 2D,但概念并不受限制。

理解最大池化

通过卷积层使用滤波器简化图像后,您在过滤后的图形中留下了大量空白空间。此外,由于所有图像滤波器,输入参数的数量显着增加。

最大池化是简化图像中识别出的最活跃特征的一种方法。

最大池化快速总结

为了压缩生成的图像大小,您可以使用最大池化来减少输出。简单地说,最大池化是将窗口中最活跃的像素保留为该窗口中所有像素块的表示。然后您滑动窗口并取其中的最大值。只要窗口的步幅大于 1,这些结果就会汇总在一起,以生成一个更小的图像。

以下示例通过取每个子方块中的最大数来将图像的大小分成四分之一。研究图 10-5 中的插图。

最大池化演示

图 10-5。2 x 2 核和步幅为 2 的最大池

在图 10-5 中的kernelSize为 2 x 2。因此,四个左上角的方块一起进行评估,从数字[12, 5, 11, 7]中,最大的是12。这个最大数传递给结果。步幅为 2,核窗口的方块完全移动到前一个方块的相邻位置,然后重新开始使用数字[20, 0, 12, 3]。这意味着每个窗口中最强的激活被传递下去。

你可能会觉得这个过程会切割图像并破坏内容,但你会惊讶地发现,生成的图像是相当容易识别的。最大池化甚至强调了检测,并使图像更容易识别。参见图 10-6,这是在之前的南瓜灯卷积上运行最大池的结果。

最大池化强调检测

图 10-6。卷积的 2 x 2 核最大池结果

虽然图 10-4 和图 10-6 在插图目的上看起来相同大小,但后者由于池化过程而稍微清晰一些,并且是原始大小的四分之一。

添加最大池化层

类似于conv2d,最大池化被添加为一层,通常紧跟在卷积之后:

tf.layers.maxPooling2d({
  poolSize: 2, // ①
  strides: 2   // ②
})

poolSize是窗口大小,就像kernelSize一样。之前的例子都是 2(代表 2 x 2)。

strides是在每次操作中向右和向下移动窗口的距离。这也可以写成strides: [2, 2]

通常,阅读图像的模型会有几层卷积,然后池化,然后再次卷积和池化。这会消耗图像的特征,并将它们分解成可能识别图像的部分。⁠²

训练图像分类

经过几层卷积和池化后,你可以将结果滤波器展平或序列化成一个单一链,并将其馈送到一个深度连接的神经网络中。这就是为什么人们喜欢展示 MNIST 训练示例;它是如此简单,以至于你实际上可以在一个图像中观察数据。

看一下使用卷积和最大池化对数字进行分类的整个过程。图 10-7 应该从底部向顶部阅读。

MNIST 逐层输出

图 10-7。MNIST 处理数字五

如果你跟随图 10-7 中显示的这幅图像的过程,你会看到底部的输入,然后是该输入与六个滤波器的卷积直接在其上方。接下来,这六个滤波器被最大池化或“下采样”,你可以看到它们变小了。然后再进行一次卷积和池化,然后将它们展平到一个完全连接的密集网络层。在展平的层上方是一个密集层,顶部的最后一个小层是一个具有 10 个可能解的 softmax 层。被点亮的是“五”。

从鸟瞰视角看,卷积和池化看起来像魔术,但它将图像的特征消化成神经元可以识别的模式。

在分层模型中,这意味着第一层通常是卷积和池化风格的层,然后它们被传递到神经网络中。图 10-8 展示了这个过程的高层视图。以下是三个阶段:

  1. 输入图像

  2. 特征提取

  3. 深度连接的神经网络

CNN 流程图

图 10-8。CNN 的三个基本阶段

处理图像数据

使用图像进行训练的一个缺点是数据集可能非常庞大且难以处理。数据集通常很大,但对于图像来说,它们通常是巨大的。这也是为什么相同的视觉数据集一遍又一遍地被使用的另一个原因。

即使图像数据集很小,当加载到内存张量形式时,它可能占用大量内存。你可能需要将训练分成张量的块,以处理庞大的图像集。这可能解释了为什么像 MobileNet 这样的模型被优化为今天标准下被认为相对较小的尺寸。在所有图像上增加或减少一个像素会导致指数级的尺寸差异。由于数据的本质,灰度张量在内存中是 RGB 图像的三分之一大小,是 RGBA 图像的四分之一大小。

分拣帽

现在是时候进行你的第一个卷积神经网络了。对于这个模型,你将训练一个 CNN 来将灰度绘画分类为 10 个类别。

如果你是 J.K.罗琳的流行书系列《哈利·波特》的粉丝,这将是有意义且有趣的。然而,如果你从未读过一本《哈利·波特》的书或者看过任何一部电影,这仍然是一个很好的练习。在书中,魔法学校霍格沃茨有四个学院,每个学院都有与之相关联的动物。你将要求用户画一幅图片,并使用该图片将它们分类到各个学院。我已经准备了一个数据集,其中的绘画在某种程度上类似于每个组的图标和动物。

我准备的数据集是从Google 的 Quick, Draw!数据集中的一部分绘画中制作的。类别已经缩减到 10 个,并且数据已经经过了大幅清理。

与本章相关的代码可以在chapter10/node/node-train-houses找到,你会发现一个包含数万个 28 x 28 绘画的 ZIP 文件,包括以下内容:

  1. 鸟类

  2. 猫头鹰

  3. 鹦鹉

  4. 蜗牛

  5. 狮子

  6. 老虎

  7. 浣熊

  8. 松鼠

  9. 头巾

这些绘画变化很大,但每个类别的特征是可辨认的。这里是一些涂鸦的随机样本,详见图 10-9。一旦你训练了一个模型来识别这 10 个类别中的每一个,你就可以使用该模型将类似特定动物的绘画分类到其相关联的学院。鸟类去拉文克劳,狮子和老虎去格兰芬多,等等。

霍格沃茨学院绘画网格形式

图 10-9. 绘画的 10 个类别

处理这个问题的方法有很多,但最简单的方法是使用 softmax 对最终层进行模型分类。正如你记得的那样,softmax 会给我们 N 个数字,它们的总和都为一。例如,如果一幅图是 0.67 的鸟,0.12 的猫头鹰,和 0.06 的鹦鹉,因为它们都代表同一个学院,我们可以将它们相加,结果总是小于一。虽然你熟悉使用返回这种结果的模型,但这将是你从头开始创建的第一个 softmax 分类模型。

入门

有几种方法可以使用 TensorFlow.js 来训练这个模型。将几兆字节的图像加载到浏览器中可以通过几种方式完成:

  • 你可以使用后续的 HTTP 请求加载每个图像。

  • 你可以将训练数据合并到一个大的精灵表中,然后使用你的张量技能来提取和堆叠每个图像到 X 和 Y 中。

  • 你可以将图像加载到 CSV 中,然后将它们转换为张量。

  • 你可以将图像进行 Base64 编码,并从单个 JSON 文件加载它们。

你在这里看到的一个常见问题是,你必须做一些额外的工作,将数据加载到浏览器的沙盒中。因此,最好使用 Node.js 进行具有大规模数据集的图像训练。我们将在本书后面讨论这种情况不那么重要的情况。

与本章相关的 Node.js 代码包含了你需要的训练数据。你会在存储库中看到一个接近 100MB 的文件(GitHub 对单个文件的限制),你需要解压到指定位置(见图 10-10)。

解压文件.zip 截图

图 10-10. 将图像解压缩到文件夹中

现在你有了图片,也知道如何在 Node.js 中读取图片,训练这个模型的代码会类似于示例 10-1。

示例 10-1. 理想的设置
  // Read images
  const [X, Y] = await folderToTensors() // ①

  // Create layers model
  const model = getModel() // ②

  // Train
  await model.fit(X, Y, {
    batchSize: 256,
    validationSplit: 0.1,
    epochs: 20,
    shuffle: true, // ③
  })

  // Save
  model.save('file://model_result/sorting_hat') // ④

  // Cleanup!
  tf.dispose([X, Y, model])
  console.log('Tensors in memory', tf.memory().numTensors)

创建一个简单的函数将图片加载到所需的 X 和 Y 张量中。

创建一个适合的 CNN 层模型。

使用shuffle属性,该属性会对当前批次进行混洗。

将生成的训练模型保存在本地。

注意

示例 10-1 中的代码没有提及设置任何测试数据。由于这个项目的性质,真正的测试将在绘制图像并确定每个笔画如何使图像更接近或更远离期望目标时进行。在训练中仍将使用验证集。

转换图像文件夹

folderToTensors函数需要执行以下操作:

  1. 识别所有 PNG 文件路径。

  2. 收集图像张量和答案。

  3. 随机化两组数据。

  4. 归一化和堆叠张量。

  5. 清理并返回结果。

要识别和访问所有图像,可以使用类似glob的库,它接受一个给定的路径,如files/**/.png*,并返回一个文件名数组。/**会遍历该文件夹中的所有子文件夹,并找到每个文件夹中的所有 PNG 文件。

你可以通过以下方式使用 NPM 安装glob

$ npm i glob

现在节点模块可用,可以被要求或导入:

const glob = require('glob')
// OR
import { default as glob } from 'glob'

由于 glob 是通过回调来操作的,你可以将整个函数包装在 JavaScript promise 中,以将其转换为异步/等待。如果你对这些概念不熟悉,可以随时查阅相关资料或仅仅学习本章提供的代码。

在收集了一组文件位置之后,你可以加载文件,将其转换为张量,甚至通过查看图像来自哪个文件夹来确定每个图像的“答案”或“y”。

记住,每次需要修改张量时都会创建一个新张量。因此,最好将张量保存在 JavaScript 数组中,而不是在进行归一化和堆叠张量时逐步进行。

将每个字符串读入这两个数组的过程可以通过以下方式完成:

files.forEach((file) => {
  const imageData = fs.readFileSync(file)
  const answer = encodeDir(file)
  const imageTensor = tf.node.decodeImage(imageData, 1)

  // Store in memory
  YS.push(answer)
  XS.push(imageTensor)
})

encodeDir函数是我编写的一个简单函数,用于查看每个图像的路径并返回一个相关的预测数字:

function encodeDir(filePath) {
  if (filePath.includes('bird')) return 0
  if (filePath.includes('lion')) return 1
  if (filePath.includes('owl')) return 2
  if (filePath.includes('parrot')) return 3
  if (filePath.includes('raccoon')) return 4
  if (filePath.includes('skull')) return 5
  if (filePath.includes('snail')) return 6
  if (filePath.includes('snake')) return 7
  if (filePath.includes('squirrel')) return 8
  if (filePath.includes('tiger')) return 9

  // Should never get here
  console.error('Unrecognized folder')
  process.exit(1)
}

一旦将图片转换为张量形式,你可能会考虑堆叠和返回它们,但在此之前至关重要的是在混洗之前对它们进行混洗。如果不混合数据,你的模型将会以最奇怪的方式快速训练。请容我用一个奇特的比喻。

想象一下,如果我让你在一堆形状中指出我在想的形状。你很快就会发现我总是在想圆形,然后你开始获得 100%的准确率。在我们的第三次测试中,我开始说,“不,那不是正方形!你错得很离谱。”于是你开始指向正方形,再次获得 100%的准确率。每三次测试,我都会改变形状。虽然你的分数超过 99%的准确率,但你从未学会选择哪个形状的实际指标。因此,当形状每次都在变化时,你就会失败。你从未学会指标,因为数据没有被混洗。

未经混洗的数据将产生相同的效果:训练准确率接近完美,而验证和测试分数则很差。即使你对每个数据集进行混洗,大部分时间你只会对相同的 256 个值进行混洗。

要对 X 和 Y 进行相同的排列混洗,可以使用tf.utils.shuffleCombo我听说向 TensorFlow.js 添加此功能的人非常酷。

// Shuffle the data (keep XS[n] === YS[n])
tf.util.shuffleCombo(XS, YS)

由于这是对 JavaScript 引用进行混洗,因此在此混洗中不会创建新的张量。

最后,您将希望将答案从整数转换为独热编码。独热编码是因为您的模型将使用 softmax,即 10 个值相加为 1,其中正确答案是唯一的主导值。

TensorFlow.js 有一个名为oneHot的方法,它将数字转换为独热编码的张量值。例如,从 5 个可能类别中的数字3将被编码为张量[0,0,1,0,0]。这就是我们希望格式化答案以匹配分类模型预期输出的方式。

现在,您可以将 X 和 Y 数组值堆叠成一个大张量,并通过除以255来将图像归一化为值0-1。堆叠和编码看起来像这样:

// Stack values
console.log('Stacking')
const X = tf.stack(XS)
const Y = tf.oneHot(YS, 10)

console.log('Images all converted to tensors:')
console.log('X', X.shape)
console.log('Y', Y.shape)

// Normalize X to values 0 - 1
const XNORM = X.div(255)
// cleanup
tf.dispose([XS, X])

由于处理数千个图像,您的计算机可能会在每个日志之间暂停。代码打印以下内容:

Stacking
Images all converted to tensors:
X [ 87541, 28, 28, 1 ]
Y [ 87541, 10 ]

现在我们有了用于训练的 X 和 Y,它们的形状是我们将创建的模型的输入和输出形状。

CNN 模型

现在是创建卷积神经网络模型的时候了。该模型的架构将是三对卷积和池化层。在每个新的卷积层上,我们将使滤波器的数量加倍。然后我们将将模型展平为一个具有 128 个单元的单个密集隐藏层,具有tanh激活,并以具有 softmax 激活的 10 个可能输出的最终层结束。如果您对为什么使用 softmax 感到困惑,请回顾我们在第九章中介绍的分类模型的结构。

您应该能够仅通过描述编写模型层,但这里是创建所描述的顺序模型的代码:

const model = tf.sequential()

// Conv + Pool combo
model.add(
  tf.layers.conv2d({
    filters: 16,
    kernelSize: 3,
    strides: 1,
    padding: 'same',
    activation: 'relu',
    kernelInitializer: 'heNormal',
    inputShape: [28, 28, 1],
  })
)
model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }))

// Conv + Pool combo
model.add(
  tf.layers.conv2d({
    filters: 32,
    kernelSize: 3,
    strides: 1,
    padding: 'same',
    activation: 'relu',
  })
)
model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }))

// Conv + Pool combo
model.add(
  tf.layers.conv2d({
    filters: 64,
    kernelSize: 3,
    strides: 1,
    padding: 'same',
    activation: 'relu',
  })
)
model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }))

// Flatten for connecting to deep layers
model.add(tf.layers.flatten())

// One hidden deep layer
model.add(
  tf.layers.dense({
    units: 128,
    activation: 'tanh',
  })
)
// Output
model.add(
  tf.layers.dense({
    units: 10,
    activation: 'softmax',
  })
)

这个新的用于非二进制分类数据的最终层意味着您需要将损失函数从binaryCrossentropy更改为categoricalCrossentropy。因此,现在model.compile的代码看起来像这样:

model.compile({
  optimizer: 'adam',
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
})

让我们通过我们学到的关于卷积和最大池化的知识来审查model.summary()方法,以确保我们已经正确构建了一切。您可以在示例 10-2 中看到结果的打印输出。

示例 10-2。model.summary()的输出
_________________________________________________________________
Layer (type)                 Output shape              Param #
=================================================================
conv2d_Conv2D1 (Conv2D)      [null,28,28,16]           160         // ①
_________________________________________________________________
max_pooling2d_MaxPooling2D1  [null,14,14,16]           0           // ②
_________________________________________________________________
conv2d_Conv2D2 (Conv2D)      [null,14,14,32]           4640        // ③
_________________________________________________________________
max_pooling2d_MaxPooling2D2  [null,7,7,32]             0
_________________________________________________________________
conv2d_Conv2D3 (Conv2D)      [null,7,7,64]             18496       // ④
_________________________________________________________________
max_pooling2d_MaxPooling2D3  [null,3,3,64]             0
_________________________________________________________________
flatten_Flatten1 (Flatten)   [null,576]                0           // ⑤
_________________________________________________________________
dense_Dense1 (Dense)         [null,128]                73856       // ⑥
_________________________________________________________________
dense_Dense2 (Dense)         [null,10]                 1290        // ⑦
=================================================================
Total params: 98442
Trainable params: 98442
Non-trainable params: 0
_________________________________________________________________

第一个卷积层的输入为[stacksize, 28, 28, 1],卷积输出为[stacksize, 28, 28, 16]。大小相同是因为我们使用了padding: 'same',而 16 是当我们指定filters: 16时得到的 16 个不同的滤波器结果。您可以将其视为每个堆栈中的每个图像的 16 个新滤波图像。这为网络提供了 160 个新参数进行训练。可训练参数的计算方式为(图像数量) * (卷积核窗口) * (输出图像) + (输出图像),计算结果为1 * (3x3) * 16 + 16 = 160

最大池化将滤波后的图像行和列大小减半,从而将像素分成四分之一。由于算法是固定的,因此此层没有任何可训练参数。

卷积和池化再次发生,并且在每个级别都使用更多的滤波器。图像的大小正在缩小,可训练参数的数量迅速增长,即16 * (3x3) * 32 + 32 = 4,640

在这里,有一个最终的卷积和池化。池化奇数会导致大于 50%的减少。

将 64 个 3 x 3 图像展平为一个由 576 个单元组成的单层。

这 576 个单元中的每一个都与 128 个单元的层密切连接。使用传统的线+节点计算,这将得到(576 * 128) + 128 = 73,856个可训练参数。

最后,最后一层有 10 个可能的值对应每个类别。

您可能想知道为什么我们评估model.summary()而不是检查正在发生的事情的图形表示。即使在较低的维度,图形表示正在发生的事情也很难说明。我已经尽力在图 10-11 中创建了一个相对详尽的插图。

CNN 神经网络

图 10-11. 每一层的可视化

与以往的神经网络图不同,CNN 的可视化解释可能有些局限性。堆叠在一起的滤波图像只能提供有限的信息。卷积过程的结果被展平并连接到一个深度连接的神经网络。您已经达到了一个复杂性的程度,summary()方法是理解内容的最佳方式。

提示

如果您想要一个动态的可视化,并观看每个训练滤波器在每一层的激活结果,数据科学 Polo Club 创建了一个美丽的CNN 解释器在 TensorFlow.js 中。查看交互式可视化器

你已经到了那里。您的结果[3, 3, 64]在连接到神经网络之前展平为 576 个人工神经元。您不仅创建了图像特征,还简化了一个[28, 28, 1]图像的输入,原本需要 784 个密集连接的输入。有了这种更先进的架构,您可以从folderToTensors()加载数据并创建必要的模型。您已经准备好训练了。

训练和保存

由于这是在 Node.js 中进行训练,您将不得不直接在机器上设置 GPU 加速。这通常是通过 NVIDIA CUDA 和 CUDA 深度神经网络(cuDNN)完成的。如果您想使用@tensorflow/tfjs-node-gpu进行训练并获得比普通tfjs-node更快的速度提升,您将不得不正确设置 CUDA 和 cuDNN 以与您的 GPU 一起工作。请参阅图 10-12。

CUDA GPU 截图

图 10-12. 使用 GPU 可以提高 3-4 倍的速度

在 20 个时代之后,生成的模型在训练中的准确率约为 95%,在验证集中的准确率约为 90%。生成模型的文件大小约为 400 KB。您可能已经注意到训练集的准确率不断上升,但验证集有时会下降。不管好坏,最后一个时代将是保存的模型。如果您想确保最高可能的验证准确性,请查看最后的章节挑战。

注意

如果您对这个模型运行了太多时代,模型将过拟合,并接近 100%的训练准确率,而验证准确率会降低。

测试模型

要测试这个模型,您需要用户的绘图。您可以在网页上创建一个简单的绘图表面,使用一个画布。画布可以订阅鼠标按下时、鼠标沿着画布移动时以及鼠标释放时的事件。使用这些事件,您可以从一个点绘制到另一个点。

构建一个草图板

您可以使用这三个事件构建一个简单的可绘制画布。您将使用一些新方法来移动画布路径和绘制线条,但这是相当易读的。以下代码设置了一个:

const canvas = document.getElementById("sketchpad");
const context = canvas.getContext("2d");
context.lineWidth = 14;
context.lineCap = "round";
let isIdle = true;

function drawStart(event) {
  context.beginPath();
  context.moveTo(
    event.pageX - canvas.offsetLeft,
    event.pageY - canvas.offsetTop
  );
  isIdle = false;
}
function drawMove(event) {
  if (isIdle) return;
  context.lineTo(
    event.pageX - canvas.offsetLeft,
    event.pageY - canvas.offsetTop
  );
  context.stroke();
}
function drawEnd() { isIdle = true; }
// Tie methods to events
canvas.addEventListener("mousedown", drawStart, false);
canvas.addEventListener("mousemove", drawMove, false);
canvas.addEventListener("mouseup", drawEnd, false);

这些图纸是由一堆较小的线条制成的,线条的笔画宽度为 14 像素,并且在边缘自动圆润。您可以在图 10-13 中看到一个测试绘图。

示例绘图

图 10-13. 运行得足够好

当用户在画布上单击鼠标时,任何移动都将从一个点绘制到新点。每当用户停止绘制时,将调用drawEnd函数。您可以添加一个按钮来对画布进行分类,或者直接将其连接到drawEnd函数并对图像进行分类。

阅读草图板

当你在画布上调用 tf.browser.fromPixels 时,你会得到 100% 的黑色像素。为什么会这样?答案是画布的某些地方没有绘制任何内容,而其他地方是黑色像素。当画布转换为张量值时,它会将空白转换为黑色。画布可能看起来有白色背景,但实际上是透明的,会显示底部的任何颜色或图案(参见 图 10-14)。

一个空的画布

图 10-14. 一个画布是透明的,所以空白像素为零

为了解决这个问题,你可以添加一个清除函数,在画布上绘制一个大的白色正方形,这样黑色线条就会在白色背景上,就像训练图像一样。这也是你可以在绘画之间清除画布的函数。要用白色背景填充画布,你可以使用 fillRect 方法,就像你在 第六章 中用来勾画标签的方法一样。

context.fillStyle = "#fff";
context.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight);

一旦画布用白色背景初始化,你就可以对画布绘制进行预测了。

async function makePrediction(canvas, model) {
  const drawTensor = tf.browser.fromPixels(canvas, 1) // ①
  const resize = tf.image.resizeNearestNeighbor(drawTensor, [28,28], true) // ②

  const batched = resize.expandDims() // ③
  const results = await model.predict(batched)
  const predictions = await results.array()

  // Display
  displayResults(predictions[0]) // ④
  // Cleanup
  tf.dispose([drawTensor, resize, batched, results])
}

当你读取画布时,不要忘记标识你只对单个通道感兴趣;否则,你需要在继续之前将 3D 张量转换为 1D 张量。

使用最近邻算法将图像调整为 28 x 28 的大小,以输入到模型中。最近邻引起的像素化在这里并不重要,所以这是一个明智的选择,因为它比 resizeBilinear 更快。

模型期望一个批次数据,所以准备数据作为一个批次的数据。这将创建一个 [1, 28, 28, 1] 的输入张量。

预测结果已经作为一个包含 10 个数字的批次返回到 JavaScript。想出一种创造性的方式来展示结果。

现在你已经得到了结果,你可以以任何你喜欢的格式展示答案。我个人是按照房间组织了分数,并用它们来设置标签的不透明度。这样,你可以在每画一条线时得到反馈。标签的不透明度取决于值 0-1,这与 softmax 预测结果非常契合。

function displayResults(predictions) {
  // Get Scores
  const ravenclaw = predictions[0] + predictions[2] + predictions[3]
  const gryffindor = predictions[1] + predictions[9]
  const hufflepuff = predictions[4] + predictions[8]
  const slytherin = predictions[6] + predictions[7]
  const deatheater = predictions[5]

  document.getElementById("ravenclaw").style.opacity = ravenclaw
  document.getElementById("gryffindor").style.opacity = gryffindor
  document.getElementById("hufflepuff").style.opacity = hufflepuff
  document.getElementById("slytherin").style.opacity = slytherin

  // Harry Potter fans will enjoy this one
  if (deatheater > 0.9) {
    alert('DEATH EATER DETECTED!!!')
  }
}

你可能会想知道拉文克劳是否有轻微的数学优势,因为它由更多类别组成,你是对的。在所有条件相同的情况下,一组完全随机的线更有可能被分类为拉文克劳,因为它拥有大多数类别。然而,当图像不是随机的时,这在统计上是不显著的。如果你希望模型只有九个类别,可以移除 bird 并重新训练,以创建最平衡的分类谱。

提示

如果你有兴趣确定哪些类别可能存在问题或混淆,你可以使用视觉报告工具,如混淆矩阵或 t-SNE 算法。这些工具对于评估训练数据特别有帮助。

我强烈建议你从chapter10/simple/simplest-draw加载本章的代码,并测试一下你的艺术技能!我的鸟类绘画将我分类到了拉文克劳,如 图 10-15 所示。

网页正确识别出一只鸟

图 10-15. 一个 UI 和绘画杰作

我能够糟糕地画出并被正确分类到其他可能的房间中。然而,我不会再用我的“艺术”来惩罚你。

章节回顾

你已经在视觉数据上训练了一个模型。虽然这个数据集仅限于灰度图,但你所学到的经验可以适用于任何图像数据集。有很多优秀的图像数据集可以与你创建的模型完美配合。我们将在接下来的两章中详细介绍。

我为本章中的绘画识别特色创建了一个更加复杂的页面。

章节挑战:保存魔法

如果您最感兴趣的是获得最高验证准确性模型,那么您的最佳模型版本很可能不是最后一个版本。例如,如果您查看图 10-16,90.3%的验证准确性会丢失,最终验证模型为 89.6%。

对于本章挑战,与其保存模型的最终训练版本,不如添加一个回调函数,当验证准确性达到新的最佳记录时保存模型。这种代码非常有用,因为它允许您运行多个时期。随着模型过拟合,您将能够保留最佳的通用模型用于生产。

验证与训练准确性

图 10-16。评估哪个准确性更重要

您可以在附录 B 中找到此挑战的答案。

复习问题

让我们回顾一下你在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. 卷积层有许多可训练的什么,可以帮助提取图像的特征?

  2. 控制卷积窗口大小的属性名称是什么?

  3. 如果你希望卷积结果与原始图像大小相同,应该将填充设置为什么?

  4. 真或假?在将图像插入卷积层之前,必须将其展平。

  5. 在 81 x 81 图像上,步幅为 3 的最大池 3 x 3 的输出大小将是多少?

  6. 如果您要对数字 12 进行独热编码,您是否有足够的信息来这样做?

这些练习的解决方案可以在附录 A 中找到。

¹ YouTube 上的3Blue1Brown的视频和讲座是任何想要深入了解卷积的人的绝佳起点。

² TensorFlow.js 中还有其他可用于实验的池化方法。

第十一章:迁移学习

“向他人的错误学习。你活不到足够长的时间来犯所有的错误。”

—埃莉诺·罗斯福

拥有大量数据、经过实战检验的模型结构和处理能力可能是具有挑战性的。能不能简单点?在第七章中,您可以使用 Teachable Machine 将训练好的模型的特质转移到新模型中,这是非常有用的。事实上,这是机器学习世界中的一个常见技巧。虽然 Teachable Machine 隐藏了具体细节,只提供了一个模型,但您可以理解这个技巧的原理,并将其应用于各种酷炫的任务。在本章中,我们将揭示这个过程背后的魔法。虽然我们将简单地以 MobileNet 为例,但这可以应用于各种模型。

迁移学习是指将训练好的模型重新用于第二个相关任务。

使用迁移学习为您的机器学习解决方案带来一些可重复的好处。大多数项目出于以下原因利用一定程度的迁移学习:

  • 重复使用经过实战检验的模型结构

  • 更快地获得解决方案

  • 通过较少的数据获得解决方案

在本章中,您将学习几种迁移学习策略。您将专注于 MobileNet 作为一个基本示例,可以以各种方式重复使用来识别各种新类别。

我们将:

  • 回顾迁移学习的工作原理

  • 看看如何重复使用特征向量

  • 将模型切割成层并重构新模型

  • 了解 KNN 和延迟分类

完成本章后,您将能够将长时间训练并具有大量数据的模型应用于您自己的需求,即使只有较小的数据集。

迁移学习是如何工作的?

一个经过不同数据训练的模型如何突然对您的数据起作用?听起来像奇迹,但这在人类中每天都发生。

您花了多年时间识别动物,可能看过数百只骆驼、天竺鼠和海狸的卡通、动物园和广告。现在我将向您展示一种您可能很少见到甚至从未见过的动物。图 11-1 中的动物被称为水豚(Hydrochoerus hydrochaeris)。

水豚的侧面

图 11-1。水豚

对于你们中的一些人,这可能是第一次(或者是少数几次)看到水豚的照片。现在,看看图 11-2 中的阵容。你能找到水豚吗?

三种哺乳动物测验

图 11-2。哪一个是水豚?

一张单独的照片的训练集足以让您做出选择,因为您一生中一直在区分动物。即使是新的颜色、角度和照片尺寸,您的大脑可能也能绝对确定地检测到动物 C 是另一只水豚。您多年的经验学习到的特征帮助您做出了明智的决定。同样地,具有丰富经验的强大模型可以被教导从少量新数据中学习新事物。

迁移学习神经网络

让我们暂时回到 MobileNet。MobileNet 模型经过训练,可以识别区分一千种物品之间的特征。这意味着有卷积来检测毛发、金属、圆形物体、耳朵以及各种关键的差异特征。所有这些特征在被压缩和简化之前都被吸收到一个神经网络中,各种特征的组合形成了分类。

MobileNet 模型可以识别不同品种的狗,甚至可以区分马耳他犬和西藏犬。如果您要制作一个“狗还是猫”分类器,那么在您更简单的模型中,大多数这些高级特征是可以重复使用的。

先前学习的卷积滤波器在识别全新分类的关键特征方面非常有用,就像我们在图 11-2 中的水豚示例一样。关键是将模型的特征识别部分提取出来,并将自己的神经网络应用于卷积输出,如图 11-3 所示。

更改 NN 流程图

图 11-3。CNN 迁移学习

那么如何分离和重新组合先前训练模型的这些部分呢?您有很多选择。再次,我们将学习更多关于图和层模型的知识。

简单的 MobileNet 迁移学习

幸运的是,TensorFlow Hub已经有一个与任何神经网络断开连接的 MobileNet 模型。它为您提供了一半的模型用于迁移学习。一半的模型意味着它还没有被绑定到最终的 softmax 层来进行分类。这使我们可以让 MobileNet 推导出图像的特征,然后为我们提供张量,然后我们可以将这些张量传递给我们自己训练的网络进行分类。

TFHub 将这些模型称为图像特征向量模型。您可以缩小搜索范围,只显示这些模型,或者通过查看问题域标签来识别它们,如图 11-4 所示。

正确标签的截图

图 11-4。图像特征向量的问题域标签

您可能会注意到 MobileNet 的小变化,并想知道差异是什么。一旦您了解了一些诡计术语,每个模型描述都会变得非常可读。

例如,我们将使用示例 11-1。

示例 11-1。图像特征向量模型之一
imagenet/mobilenet_v2_130_224/feature_vector

imagenet

这个模型是在 ImageNet 数据集上训练的。

mobilenet_v2

该模型的架构是 MobileNet v2。

130

该模型的深度乘数为 1.30。这会产生更多的特征。如果您想加快速度,可以选择“05”,这将减少一半以下的特征输出并提高速度。这是一个微调选项,当您准备好修改速度与深度时可以使用。

224

该模型的预期输入尺寸为 224 x 224 像素的图像。

feature_vector

我们已经从标签中了解到,但这个模型输出的张量是为了作为图像特征的第二个模型来解释。

现在我们有一个经过训练的模型,可以识别图像中的特征,我们将通过 MobileNet 图像特征向量模型运行我们的训练数据,然后在输出上训练一个模型。换句话说,训练图像将变成一个特征向量,我们将训练一个模型来解释该特征向量。

这种策略的好处是实现起来很简单。主要缺点是当您准备使用新训练的模型时,您将不得不加载两个模型(一个用于生成特征,一个用于解释)。创造性地,可能有一些情况下“特征化”图像然后通过多个神经网络运行可能非常有用。无论如何,让我们看看它的实际效果。

TensorFlow Hub 检查,对手!

我们将使用 MobileNet 进行迁移学习,以识别像图 11-5 中所示的国际象棋棋子。

桌子上的国际象棋骑士的图像

图 11-5。简单的国际象棋棋子分类器

您只会有每个国际象棋棋子的几张图像。通常这是不够的,但通过迁移学习的魔力,您将得到一个高效的模型。

加载国际象棋图像

为了这个练习,我已经编译了一个包含 150 张图像的集合,并将它们加载到 CSV 文件中以便快速使用。在大多数情况下,我不建议这样做,因为这对于处理和磁盘空间是低效的,但它可以作为一种简单的向量,用于一些快速的浏览器训练。现在加载这些图像的代码是微不足道的。

注意

你可以访问象棋图像和将它们转换为 CSV 文件的代码在chapter11/extra/node-make-csvs文件夹中。

文件 chess_labels.csvchess_images.csv 可以在与本课程相关的chess_data.zip文件中找到。解压这个文件并使用 Danfo.js 加载内容。

许多浏览器可能会在同时读取所有 150 个图像时出现问题,所以我限制了演示只处理 130 个图像。与并发数据限制作斗争是机器学习中常见的问题。

注意

一旦图像被提取特征,它所占用的空间就会少得多。随意尝试批量创建特征,但这超出了本章的范围。

图像已经是 224 x 224,所以你可以用以下代码加载它们:

console.log("Loading huge CSV - this will take a while");
const numImages = 130; // between 1 and 150 // Get Y values const labels = await dfd.read_csv("chess_labels.csv", numImages); // ①
const Y = labels.tensor; // ②
// Get X values (Chess images) const chessImages = await dfd.read_csv("chess_images.csv", numImages);
const chessTensor = chessImages.tensor.reshape([
  labels.shape[0], 224, 224, 3, // ③
]);
console.log("Finished loading CSVs", chessTensor.shape, Y.shape);

read_csv的第二个参数限制了行数到指定的数字。

然后将 DataFrames 转换为张量。

图像被展平以变成序列化,但现在被重新塑造成一个四维的 RGB 图像批次。

经过一段时间,这段代码会打印出 130 个准备好的图像和编码的 X 和 Y 形状:

Finished loading CSVs (4) [130, 224, 224, 3] (2) [130, 6]

如果你的计算机无法处理 130 个图像,你可以降低numImages变量,仍然可以继续。然而,CSV 文件的加载时间始终是恒定的,因为整个文件必须被处理。

提示

象棋棋子这样的图像非常适合进行图像增强,因为扭曲棋子永远不会导致一个棋子被误认为是另一个。如果你需要更多的图像,你可以镜像整个集合,有效地将你的数据翻倍。存在整个库来镜像、倾斜和扭曲图像,这样你就可以创建更多数据。

加载特征模型

你可以像加载 TensorFlow Hub 中的任何模型一样加载特征模型。你可以通过模型进行预测,结果将是numImages个预测。代码看起来像示例 11-2。

示例 11-2. 加载和使用特征向量模型
// Load feature model
const tfhubURL =
  "https://oreil.ly/P2t2k";
const featureModel = await tf.loadGraphModel(tfhubURL, {
  fromTFHub: true,
});
const featureX = featureModel.predict(chessTensor);
// Push data through feature detection
console.log(`Features stack ${featureX.shape}`);

控制台日志的输出是

Features stack 130,1664

每个 130 个图像已经变成了一组 1,664 个浮点值,这些值对图像的特征敏感。如果你改变模型以使用不同的深度,特征的数量也会改变。1,664 这个数字是 MobileNet 1.30 深度版本独有的。

如前所述,1,664 个Float32特征集比每个图像的224*224*3 = 150,528Float32输入要小得多。这将加快训练速度,并对计算机内存更友好。

创建你自己的神经网络

现在你有了一组特征,你可以创建一个新的完全未经训练的模型,将这 1,664 个特征与你的标签相匹配。

示例 11-3. 一个包含 64 层的小模型,最后一层是 6
// Create NN const transferModel = tf.sequential({
  layers: [                              // ①
    tf.layers.dense({
      inputShape: [featureX.shape[1]],   // ②
      units: 64,
      activation: "relu",
    }),
    tf.layers.dense({ units: 6, activation: "softmax" }),
  ],
});

这个 Layers 模型使用了一个与你习惯的略有不同的语法。而不是调用.add,所有的层都被呈现在初始配置的数组中。这种语法对于像这样的小模型很好。

模型的inputShape被动态设置为1,664,以防你想通过更新模型 URL 来改变模型的深度乘数。

训练结果

在训练代码中没有什么新的。模型基于特征输出进行训练。由于特征输出与原始图像张量相比非常小,训练速度非常快。

transferModel.compile({
  optimizer: "adam",
  loss: "categoricalCrossentropy",
  metrics: ["accuracy"],
});

await transferModel.fit(featureX, Y, {
  validationSplit: 0.2,
  epochs: 20,
  callbacks: { onEpochEnd: console.log },
});

几个周期后,模型的准确率就会非常出色。查看图 11-6。

迁移学习结果

图 11-6. 从 50%到 96%的验证准确率在 20 个周期内

在 TensorFlow Hub 上使用现有模型进行迁移学习可以减轻架构方面的困扰,并为你带来高准确性。但这并不是你实现迁移学习的唯一方式。

利用 Layers 模型进行迁移学习

之前的方法存在一些明显和不那么明显的限制。首先,特征模型无法进行训练。你所有的训练都是在一个消耗图模型特征的新模型上进行的,但卷积层和大小是固定的。你可以使用卷积网络模型的小变体,但无法更新或微调它。

之前来自 TensorFlow Hub 的模型是一个图模型。图模型被优化用于速度,并且无法修改或训练。另一方面,Layers 模型则适用于修改,因此你可以将它们重新连接以进行迁移学习。

此外,在之前的示例中,每次需要对图像进行分类时,实际上都在处理两个模型。你需要加载两个 JSON 模型,并将图像通过特征模型和新模型以对图像进行分类。这并不是世界末日,但通过组合 Layers 模型可以实现单一模型。

让我们再次解决同样的国际象棋问题,但这次使用 Layers 版本的 MobileNet,这样我们可以检查差异。

在 MobileNet 上修剪层

在这个练习中,你将使用一个设置为 Layers 模型的 MobileNet v1.0 版本。这是 Teachable Machine 使用的模型,虽然对于小型探索性项目来说已经足够了,但你会注意到它不如深度为 1.30 的 MobileNet v2 准确。你已经熟悉了使用向导转换模型的方法,就像你在第七章中学到的那样,所以在需要时你可以创建一个更大、更新的 Layers 模型。准确性是一个重要的指标,但在寻找迁移模型时,它远非唯一的评估指标。

MobileNet 有大量的层,其中一些是你以前从未见过的。让我们来看一下。加载与本章相关联的 MobileNet 模型,并使用model.summary()来查看层的摘要。这会打印出一个庞大的层列表。不要感到不知所措。当你从底部向上阅读时,最后两个带有激活的卷积层被称为conv_predsconv_pw_13_relu

...

conv_pw_13 (Conv2D)          [null,7,7,256]            65536
_________________________________________________________________
conv_pw_13_bn (BatchNormaliz [null,7,7,256]            1024
_________________________________________________________________
conv_pw_13_relu (Activation) [null,7,7,256]            0
_________________________________________________________________
global_average_pooling2d_1 ( [null,256]                0
_________________________________________________________________
 reshape_1 (Reshape)          [null,1,1,256]            0
_________________________________________________________________
dropout (Dropout)            [null,1,1,256]            0
_________________________________________________________________
conv_preds (Conv2D)          [null,1,1,1000]           257000
_________________________________________________________________
act_softmax (Activation)     [null,1,1,1000]           0
_________________________________________________________________
reshape_2 (Reshape)          [null,1000]               0
=================================================================
Total params: 475544
Trainable params: 470072
Non-trainable params: 5472

最后一个卷积层conv_preds作为将特征展平到 1,000 个可能类别的flatten层。这在一定程度上是特定于模型训练的类别,因此因此我们将跳到第二个卷积层(conv_pw_13_relu)并在那里裁剪。

MobileNet 是一个复杂的模型,即使你不必理解所有的层来用它进行迁移学习,但在决定移除哪些部分时还是需要一些技巧。在更简单的模型中,比如即将到来的章节挑战中的模型,通常会保留整个卷积工作流程,并在 flatten 层进行裁剪。

你可以通过知道其唯一名称来裁剪到一个层。示例 11-4 中显示的代码在GitHub 上可用

示例 11-4。
const featureModel = await tf.loadLayersModel('mobilenet/model.json')
console.log('ORIGINAL MODEL')
featureModel.summary()
const lastLayer = featureModel.getLayer('conv_pw_13_relu')
const shavedModel = tf.model({
  inputs: featureModel.inputs,
  outputs: lastLayer.output,
})
console.log('SHAVED DOWN MODEL')
shavedModel.summary()

示例 11-4 中的代码打印出两个大模型,但关键区别在于第二个模型突然在conv_pw_13_relu处停止。

现在最后一层是我们确定的那一层。当你查看修剪后模型的摘要时,它就像一个特征提取器。有一个应该注意的关键区别。最后一层是一个卷积层,因此你构建的迁移模型的第一层应该将卷积输入展平,以便可以与神经网络进行密集连接。

层特征模型

现在你可以将修剪后的模型用作特征模型。这将为你带来与 TFHub 相同的双模型系统。你的第二个模型需要读取conv_pw_13_relu的输出:

// Create NN
const transferModel = tf.sequential({
  layers: [
    tf.layers.flatten({ inputShape: featureX.shape.slice(1) }),
    tf.layers.dense({ units: 64, activation: 'relu' }),
    tf.layers.dense({ units: 6, activation: 'softmax' }),
  ],
})

我们正在设置由中间特征定义的形状。这也可以直接与修剪模型的输出形状相关联(shavedModel.outputs[0].shape.slice(1))。

从这里开始,您又回到了 TFHub 模型的起点。基础模型创建特征,第二个模型解释这些特征。

使用这两个层进行训练可以实现大约 80%以上的准确率。请记住,我们使用的是完全不同的模型架构(这是 MobileNet v1)和较低的深度乘数。从这个粗糙模型中至少获得 80%是不错的。

统一模型

就像特征向量模型一样,您的训练只能访问几层,并且不会更新卷积层。现在您已经训练了两个模型,可以将它们的层再次统一到一个单一模型中。您可能想知道为什么在训练后而不是之前将模型合并。在训练新层时,将您的特征层锁定或“冻结”到其原始权重是一种常见做法。

一旦新层得到训练,通常可以“解冻”更多层,并一起训练新的和旧的。这个阶段通常被称为微调模型。

那么现在如何统一这两个模型呢?答案出奇地简单。创建第三个顺序模型,并使用model.add添加两个模型。代码如下:

// combine the models
const combo = tf.sequential()
combo.add(shavedModel)
combo.add(transferModel)
combo.compile({
  optimizer: 'adam',
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
})
combo.summary()

新的combo模型可以下载或进一步训练。

如果在训练新层之前将模型合并,您可能会看到您的模型过度拟合数据。

无需训练

值得注意的是,有一种巧妙的方法可以使用两个模型进行零训练的迁移学习。诀窍是使用第二个模型来识别相似性中的距离。

第二个模型称为 K-最近邻(KNN)¹模型,它将数据元素与特征空间中 K 个最相似的数据元素分组在一起。成语“物以类聚”是 KNN 的前提。

在图 11-7 中,X 将被识别为兔子,因为特征中的三个最近示例也是兔子。

特征距离

图 11-7。在特征空间中识别邻居

KNN 有时被称为基于实例的学习惰性学习,因为你将所有必要的处理移动到数据分类的时刻。这种不同的模型很容易更新。您可以始终动态地添加更多图像和类别,以定义边缘情况或新类别,而无需重新训练。成本在于特征图随着添加的每个示例而增长,而不像单个训练模型的固定空间。您向 KNN 解决方案添加的数据点越多,伴随模型的特征集就会变得越大。

此外,由于没有训练,相似性是唯一的度量标准。这使得这个系统对于某些问题来说并不理想。例如,如果您试图训练一个模型来判断人们是否戴着口罩,那么您需要一个模型专注于单个特征而不是多个特征的集合。穿着相同的两个人可能具有更多相似之处,因此可能会被放在 KNN 中的同一类别中。要使 KNN 在口罩上起作用,您的特征向量模型必须是面部特定的,训练模型可以学习区分模式。

简单的 KNN:兔子对运动汽车

KNN,就像 MobileNet 一样,由 Google 提供了一个 JS 包装器。我们可以通过隐藏所有复杂细节,使用 MobileNet 和 KNN NPM 包快速实现 KNN 迁移学习,以制作一个快速的迁移学习演示。

我们不仅要避免运行任何训练,还要使用现有库来避免深入研究 TensorFlow.js。我们将为一个引人注目的演示而这样做,但如果您决定使用这些模型构建更健壮的东西,您可能应该考虑避免使用您无法控制的抽象包。您已经了解了迁移学习的所有内部工作原理。

为了进行这个快速演示,您将导入三个 NPM 模块:

<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.7.0/dist/tf.min.js">
</script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@2.0">
</script>
<script
src="https://cdn.jsdelivr.net/npm/@tensorflow-models/knn-classifier@1.2.2">
</script>

为了简单起见,本章的示例代码中所有的图像都在页面上,因此您可以直接引用它们。现在您可以使用mobileNet = await mobilenet.load();加载 MobileNet,并使用knnClassifier.create();加载 KNN 分类器。

KNN 分类器需要每个类别的示例。为了简化这个过程,我创建了以下辅助函数:

// domID is the DOM element ID // classID is the unique class index function addExample(domID, classID) {
  const features = mobileNet.infer( // ①
    document.getElementById(domID), // ②
    true                            // ③
  );
  classifier.addExample(features, classID);
}

infer方法返回值,而不是富 JavaScript 对象的检测。

页面上的图像id将告诉 MobileNet 要调整大小和处理哪个图像。张量逻辑被 JavaScript 隐藏,但本书的许多章节已经解释了实际发生的事情。

MobileNet 模型返回图像的特征(有时称为嵌入)。如果未设置,则返回 1,000 个原始值的张量(有时称为对数)。

现在您可以使用这个辅助方法为每个类别添加示例。您只需命名图像元素的唯一 DOM ID 以及应与之关联的类别。添加三个示例就像这样简单:

// Add examples of two classes
addExample('bunny1', 0)
addExample('bunny2', 0)
addExample('bunny3', 0)
addExample('sport1', 1)
addExample('sport2', 1)
addExample('sport3', 1)

最后,预测的系统是相同的。获取图像的特征,并要求分类器根据 KNN 识别输入基于哪个类。

// Moment of truth const testImage = document.getElementById('test')
const testFeature = mobileNet.infer(testImage, true);
const predicted = await classifier.predictClass(testFeature)
if (predicted.classIndex === 0) { // ①
  document.getElementById("result").innerText = "A Bunny" // ②
} else {
  document.getElementById("result").innerText = "A Sports Car"
}

classIndex是作为addExample中传递的数字。如果添加第三个类别,那么新的索引将成为可能的输出。

网页文本从“???”更改为结果。

结果是 AI 可以通过与六个示例进行比较来识别新图像的正确类别,如图 11-8 所示。

AI 页面的截图

图 11-8. 仅有每个类别三张图像,KNN 模型预测正确

您可以动态地添加更多类别。KNN 是一种令人兴奋且可扩展的方式,通过迁移学习利用先进模型的经验。

章节回顾

因为本章已经解释了使用 MobileNet 进行迁移学习的神秘,现在您可以将这种增强功能应用于您可以在一定程度上理解的任何现有模型。也许您想调整宠物面孔模型以查找卡通或人脸。您不必从头开始!

迁移学习为您的 AI 工具箱增加了新的实用功能。现在,当您在野外找到一个新模型时,您可以问自己如何直接使用它,以及如何将其用于类似的迁移学习。

章节挑战:光速学习

上一章中的霍格沃茨分选模型在卷积层中有数千张黑白绘画图像的经验。不幸的是,这些数千张图像仅限于动物和头骨。它们与星际迷航无关。不要担心;只需使用大约 50 张新图像,您就可以重新训练上一章的模型,以识别图 11-9 中显示的三个星际迷航符号。

几个时代内的完美验证准确性

图 11-9. 星际迷航符号

将相位设置为有趣,并使用本章学到的方法来获取您在第十章中训练的 Layers 模型(或从相关的书源代码下载已训练的模型),并训练一个新模型,可以从仅有几个示例中识别这些图像。

新的训练图像数据可以在相关书籍源代码中以 CSV 形式找到。训练图像数据已经放在 CSV 中,因此您可以使用 Danfo.js 轻松导入它。文件是images.csvlabels.csv

您可以在附录 B 中找到这个挑战的答案。

复习问题

让我们回顾一下您在本章编写的代码中学到的教训。花点时间回答以下问题:

  1. KNN 代表什么?

  2. 每当您有一个小的训练集时,存在什么危险?

  3. 当您在 TensorFlow Hub 上寻找 CNN 模型的卷积部分时,您要寻找哪个标签?

  4. 哪个深度乘数会产生更广泛的特征输出,0.50 还是 1.00?

  5. 您可以调用 MobileNet NPM 模块的哪种方法来收集图像的特征嵌入?

  6. 您应该先组合您的转移模型部分然后训练,还是先训练然后组合您的模型?

  7. 当您在卷积层上切割模型时,在将该信息导入神经网络的密集层之前,您需要做什么?

这些练习的解决方案可以在附录 A 中找到。

¹ KNN 是由 Evelyn Fix 和 Joseph Hodges 于 1951 年开发的。

第十二章:骰子化:顶点项目

“每个人都有一个计划,直到他们被打在嘴巴上。”

—铁拳迈克·泰森

你的所有训练使你通过各种理论和练习。现在,你已经知道足够多,可以提出一个计划,在 TensorFlow.js 中为机器学习构建新的创意用途。在这一章中,你将开发你的顶点项目。与其用 TensorFlow.js 学习另一个机器学习方面,不如在这一章开始时接受一个挑战,并利用你现有的技能构建一个可行的解决方案。从构思到完成,这一章将指导你解决问题的执行。无论这是你第一本机器学习书籍还是第十本,这个顶点项目是你展现才华的时刻。

我们将:

  • 研究问题

  • 创建和增强数据

  • 训练一个能解决问题的模型

  • 在网站中实施解决方案

当你完成这一章时,你将运用从头到尾的技能来解决一个有趣的机器学习项目。

一个具有挑战性的任务

我们将利用你新发现的技能来模糊艺术和科学之间的界限。工程师们多年来一直在利用机器进行令人印象深刻的视觉壮举。最值得注意的是,暗箱相机技术(如图 12-1 所示)让疯狂的科学家们可以用镜头和镜子追踪实景。¹

人看着黑匣子相机暗箱

图 12-1。相机暗箱

如今,人们正在用最奇怪的东西制作艺术品。在我的大学,艺术系用便利贴像素创造了一个完整的《超级马里奥兄弟》场景。虽然我们中有些人有艺术的神启,但其他人可以通过发挥他们的其他才能制作类似的作品。

你的挑战,如果你选择接受并从这本书中学到尽可能多的东西,就是教会人工智能如何使用骰子绘画。通过排列六面骰子并选择正确的数字显示,你可以复制任何图像。艺术家们会购买数百个骰子,并利用他们的技能重新创作图像。在这一章中,你将运用你学到的所有技能,教会人工智能如何将图像分解成骰子艺术,如图 12-2 所示。

图像转换为骰子版本

图 12-2。将图形转换为骰子

一旦你的人工智能能够将黑白图像转换为骰子,你可以做很多事情,比如创建一个酷炫的网络摄像头滤镜,制作一个出色的网站,甚至为自己打印一个有趣的手工艺项目的说明。

在继续之前花 10 分钟,策划如何利用你的技能从零开始构建一个体面的图像到骰子转换器。

计划

理想情况下,你想到了与我类似的东西。首先,你需要数据,然后你需要训练一个模型,最后,你需要创建一个利用训练模型的网站。

数据

虽然骰子并不是非常复杂,但每个像素块应该是什么并不是一个现有的数据集。你需要生成一个足够好的数据集,将图像的一个像素块映射到最适合的骰子。你将创建像图 12-3 中那样的数据。

垂直线转换为骰子中的数字三

图 12-3。教 AI 如何选择哪个骰子适用

一些骰子可以旋转。数字二、三和六将需要在数据集中重复出现,因此它们对每种配置都是特定的。虽然它们在游戏中是可互换的,但在艺术中不是。图 12-4 展示了这些数字如何在视觉上镜像。

三个骰子和三个旋转

图 12-4。角度很重要;这两个不相等

这意味着你需要总共九种可能的配置。那就是六个骰子,其中三个旋转了 90 度。图 12-5 展示了你平均六面游戏骰子的所有可能配置。

用实际骰子说明的六面骰子的九种可能配置

图 12-5。九种可能的配置

这些是用一种必须平放的骰子风格重新创建任何图像的可用模式。虽然这对于直接表示图像来说并不完美,但随着数量和距离的增加,分辨率会提高。

训练

在设计模型时,会有两个重要问题:

  • 是否有什么东西对迁移学习有用?

  • 模型应该有卷积层吗?

首先,我从未见过类似的东西。在创建模型时,我们需要确保有一个验证和测试集来验证模型是否训练良好,因为我们将从头开始设计它。

其次,模型应该避免使用卷积。卷积可以帮助您提取复杂的特征,而不考虑它们的位置,但这个模型非常依赖位置。两个像素块可以是一个 2 或一个旋转的 2。对于这个练习,我将不使用卷积层。

直到完成后我们才知道跳过卷积是否是一个好计划。与大多数编程不同,机器学习架构中有一层实验。我们可以随时回去尝试其他架构。

网站

一旦模型能够将一小块像素分类为相应的骰子,您将需要激活您的张量技能,将图像分解成小块以进行转换。图像的片段将被堆叠,预测并与骰子的图片重建。

注意

由于本章涵盖的概念是先前解释的概念的应用,本章将讨论高层次的问题,并可能跳过解决这个毕业项目的代码细节。如果您无法跟上,请查看先前章节以获取概念和相关源代码的具体信息。本章不会展示每一行代码

生成训练数据

本节的目标是创建大量数据以用于训练模型。这更多是一门艺术而不是科学。我们希望有大量的数据。为了生成数百张图像,我们可以轻微修改现有的骰子像素。对于本节,我创建了 12 x 12 的骰子印刷品,使用简单的二阶张量。可以通过一点耐心创建九种骰子的配置。查看示例 12-1,注意代表骰子黑点的零块。

示例 12-1。骰子一和二的数组表示
[
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
],
[
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
],

您可以使用tf.ones创建一个[9, 12, 12]的全为1的浮点数,然后手动将位置转换为0,以制作每个骰子的黑点。

一旦您拥有所有九种配置,您可以考虑图像增强以创建新数据。标准图像增强库在这里无法使用,但您可以利用您的张量技能编写一个函数,稍微移动每个骰子位置一个像素。这种小变异将一个骰子变成九种变体。然后您的数据集中将有九种骰子的九种变体。

在代码中实现这一点,想象一下增加骰子的大小,然后在周围滑动一个 12 x 12 的窗口,稍微偏离中心剪切图像的新版本:这是一种填充和裁剪增强

const pixelShift = async (inputTensor, mutations = []) => {
  // Add 1px white padding to height and width
  const padded = inputTensor.pad( // ①
    [[1, 1],[1, 1],],
    1
  )
  const cutSize = inputTensor.shape
  for (let h = 0; h < 3; h++) {
    for (let w = 0; w < 3; w++) { // ②
      mutations.push(padded.slice([h, w], cutSize)) // ③
    }
  }
  padded.dispose()
  return mutations
}

.pad为现有张量添加一个值为1的白色边框。

为了生成九个新的移位值,每次都会移动切片位置的起点。

切片的子张量每次都会成为一个新的 12 x 12 值,起点不同。

pixelShift的结果创建了一些小变化,这些变化应该仍然可以用原始骰子解决。图 12-6 显示了移动像素的视觉表示。

从一个骰子生成九个新图像

图 12-6。移动像素创建新的骰子

虽然每个骰子有九个版本比一个好,但数据集仍然非常小。您必须想出一种方法来创建新数据。

您可以通过随机组合这九个移位图像来创建新的变体。有很多方法可以组合这些图像中的任意两个。一种方法是使用tf.where,并将两个图像中较小的保留在它们的新组合图像中。这样可以保留任意两个移位骰子的黑色像素。

// Creates combinations take any two from array // (like Python itertools.combinations) const combos = async (tensorArray) => {
  const startSize = tensorArray.length
  for (let i = 0; i < startSize - 1; i++) {
    for (let j = i + 1; j < startSize; j++) {
      const overlay = tf.tidy(() => {
        return tf.where( // ①
          tf.less(tensorArray[i], tensorArray[j]), // ②
          tensorArray[i], // ③
          tensorArray[j]  // ④
        )
      })
      tensorArray.push(overlay)
    }
  }
}

tf.where就像在每个元素上运行条件。

当第一个参数小于第二个参数时,tf.less返回 true。

如果where中的条件为 true,则返回arrCopy[i]中的值。

如果where中的条件为 false,则返回arrCopy[j]中的值。

当您重叠这些骰子时,您会得到看起来像之前骰子的小变异的新张量。骰子上的 4 x 4 个点被组合在一起,可以创建相当多的新骰子,可以添加到您的数据集中。

甚至可以对变异进行两次。变异的变异仍然可以被人眼区分。当您查看图 12-7 中生成的四个骰子时,仍然可以明显看出这些骰子是从显示值为一的一面生成的。即使它们是由虚构的第二代变异组合而成,新骰子仍然在视觉上与所有其他骰子组合明显不同。

通过组合以前的骰子进行变异以制作新骰子

图 12-7。通过骰子组合的四种变异

正如您可能已经猜到的那样,在创建这些类似俄罗斯方块的形状时,会有一些意外的重复。与其试图避免重复配置,不如通过调用tf.unique来删除重复项。

警告

目前 GPU 不支持tf.unique,因此您可能需要将后端设置为 CPU 来调用unique。之后,如果您愿意,可以将后端返回到 GPU。

在高层次上,对生成的骰子图像进行移位和变异,从单个骰子生成了两百多个骰子。以下是高层次的总结:

  1. 将图像在每个方向上移动一个像素。

  2. 将移位后的张量组合成所有可能的组合。

  3. 对先前集合执行相同的变异组合。

  4. 仅使用唯一结果合并数据。

现在,对于每种九种可能的组合,我们有两百多个张量。考虑到刚才只有九个张量,这还不错。两百张图片足够吗?我们需要测试才能找出答案。

您可以立即开始训练,或者将数据保存到文件中。本章相关的代码会写入一个文件。本节的主要功能可以用以下代码概括:

const createDataObject = async () => {
  const inDice = require('./dice.json').data
  const diceData = {}
  // Create new data from each die
  for (let idx = 0; idx < inDice.length; idx++) {
    const die = inDice[idx]
    const imgTensor = tf.tensor(die)
    // Convert this single die into 200+ variations
    const results = await runAugmentation(imgTensor, idx)
    console.log('Unique Results:', idx, results.shape)
    // Store results
    diceData[idx] = results.arraySync()
    // clean
    tf.dispose([results, imgTensor])
  }

  const jsonString = JSON.stringify(diceData)
  fs.writeFile('dice_data.json', jsonString, (err) => {
    if (err) throw err
    console.log('Data written to file')
  })
}

训练

现在您总共有将近两千张图片,可以尝试训练您的模型。数据应该被堆叠和洗牌:

const diceImages = [].concat(  // ①
  diceData['0'],
  diceData['1'],
  diceData['2'],
  diceData['3'],
  diceData['4'],
  diceData['5'],
  diceData['6'],
  diceData['7'],
  diceData['8'],
)

// Now the answers to their corresponding index const answers = [].concat(
  new Array(diceData['0'].length).fill(0),  // ②
  new Array(diceData['1'].length).fill(1),
  new Array(diceData['2'].length).fill(2),
  new Array(diceData['3'].length).fill(3),
  new Array(diceData['4'].length).fill(4),
  new Array(diceData['5'].length).fill(5),
  new Array(diceData['6'].length).fill(6),
  new Array(diceData['7'].length).fill(7),
  new Array(diceData['8'].length).fill(8),
)

// Randomize these two sets together tf.util.shuffleCombo(diceImages, answers)  // ③

通过连接单个数据数组来创建大量数据数组。

然后,您创建与每个数据集大小完全相同的答案数组,并使用Array.fill来填充它们。

然后,您可以将这两个数组一起随机化。

从这里,您可以拆分出一个测试集,也可以不拆分。如果您需要帮助,可以查看相关代码。一旦您按照自己的意愿拆分了数据,然后将这两个 JavaScript 数组转换为正确的张量:

const trainX = tf.tensor(diceImages).expandDims(3)  // ①
const trainY = tf.oneHot(answers, numOptions) // ②

创建堆叠张量,并为简单起见,通过在索引三处扩展维度将其返回为三维图像。

然后,将数字答案进行独热编码为张量,以适应 softmax 模型输出。

该模型采用了简单而小型的设计。您可能会找到更好的结构,但对于这个,我选择了两个隐藏层。随时回来并尝试使用架构进行实验,看看您可以获得什么样的速度和准确性。

const model = tf.sequential()
model.add(tf.layers.flatten({ inputShape }))
model.add(tf.layers.dense({
    units: 64,
    activation: 'relu',
}))
model.add(tf.layers.dense({
    units: 8,
    activation: 'relu',
}))
model.add(tf.layers.dense({
    units: 9,
    kernelInitializer: 'varianceScaling',
    activation: 'softmax',
}))

该模型首先通过将图像输入展平以将它们连接到神经网络,然后有一个64和一个8单元层。最后一层是九种可能的骰子配置。

这个模型在几个时代内就能达到近乎完美的准确率。这对于生成的数据来说是很有希望的,但在下一节中,我们将看到它在实际图像中的表现如何。

网站界面

现在您已经有了一个经过训练的模型,是时候用非生成数据进行测试了。肯定会有一些错误,但如果模型表现得不错,这将是相当成功的!

您的网站需要告诉需要使用多少个骰子,然后将输入图像分成相同数量的块。这些块将被调整大小为 12 x 12 的输入(就像我们的训练数据),然后在图像上运行模型进行预测。在图 12-8 中显示的示例中,一个 X 的图像被告知要转换为四个骰子。因此,图像被切割成四个象限,然后对每个象限进行预测。它们应该理想地将骰子对齐以绘制 X。

将 TensorFlow 标志切割成 32 x 32 块之前和之后

图 12-8。将 TensorFlow 标志切割成 32 x 32 块

一旦您获得了预测结果,您可以重建一个由指定图像张量组成的新张量。

注意

这些图像是在 0 和 1 上进行训练的。这意味着,为了期望得到一个体面的结果,您的输入图像也应该由 0 和 1 组成。颜色甚至灰度都会产生虚假的结果。

应用程序代码的核心应该看起来像这样:

const dicify = async () => {
  const modelPath = '/dice-model/model.json'
  const dModel = await tf.loadLayersModel(modelPath)

  const grid = await cutData("input")
  const predictions = await predictResults(dModel, grid)
  await displayPredictions(predictions)

  tf.dispose([dModel, predictions])
  tf.dispose(grid)
}

结果的预测是您经典的“数据输入,数据输出”模型行为。最复杂的部分将是cutDatadisplayPredictions方法。在这里,您的张量技能将大放异彩。

切成块

cutData方法将利用tf.split,它沿着一个轴将张量分割为 N 个子张量。您可以通过使用tf.split沿着每个轴将图像分割成一个补丁或图像网格来进行预测。

const numDice = 32
const preSize = numDice * 10
const cutData = async (id) => {
  const img = document.getElementById(id)
  const imgTensor = tf.browser.fromPixels(img, 1) // ①
  const resized = tf.image.resizeNearestNeighbor( // ②
    imgTensor, [preSize,preSize]
  )
  const cutSize = numDice
  const heightCuts = tf.split(resized, cutSize)   // ③
  const grid = heightCuts.map((sliver) =>         // ④
    tf.split(sliver, cutSize, 1))

  return grid
}

您只需要将图像的灰度版本从像素转换过来。

图像被调整大小,以便可以被所需数量的骰子均匀分割。

图像沿着第一个轴(高度)被切割。

然后将这些列沿着宽度轴切割,以创建一组张量。

grid变量现在包含一个图像数组。在需要时,您可以调整图像大小并堆叠它们进行预测。例如,图 12-9 是一个切片网格,因为 TensorFlow 标志的黑白切割将创建许多较小的图像,这些图像将被转换为骰子。

将 TensorFlow 标志切割成 27x27 块

图 12-9。黑白 TensorFlow 标志的切片

重建图像

一旦您有了预测结果,您将想要重建图像,但您将希望将原始块替换为它们预测的骰子。

从预测答案重建和创建大张量的代码可能如下所示:

const displayPredictions = async (answers) => {
  tf.tidy(() => {
    const diceTensors = diceData.map( // ①
      (dt) => tf.tensor(dt)
    )
    const { indices } = tf.topk(answers)
    const answerIndices = indices.dataSync()

    const tColumns = []
    for (let y = 0; y < numDice; y++) {
      const tRow = []
      for (let x = 0; x < numDice; x++) {
        const curIndex = y * numDice + x       // ②
        tRow.push(diceTensors[answerIndices[curIndex]])
      }
      const oneRow = tf.concat(tRow, 1)        // ③
      tColumns.push(oneRow)
    }
    const diceConstruct = tf.concat(tColumns)  // ④
    // Print the reconstruction to the canvas
    const can = document.getElementById('display')
    tf.browser.toPixels(diceConstruct, can)    // ⑤
  })
}

要绘制的diceTensorsdiceData中加载并转换。

要从 1D 返回到 2D,需要为每一行计算索引。

行是通过沿着宽度轴进行连接而创建的。

列是通过沿着默认(高度)轴连接行来制作的。

哒哒!新构建的张量可以显示出来了。

如果你加载了一个黑白图像并处理它,现在是真相的时刻。每个类别生成了大约 200 张图像是否足够?

我将numDice变量设置为 27。一个 27 x 27 的骰子图像是相当低分辨率的,需要在亚马逊上花费大约 80 美元。让我们看看加上 TensorFlow 标志会是什么样子。图 12-10 展示了结果。

TensorFlow 标志转换为 27 x 27 骰子之前和之后

图 12-10。TensorFlow 标志转换为 27 x 27 骰子

它有效!一点也不错。你刚刚教会了一个 AI 如何成为一个艺术家。如果你增加骰子的数量,图像会变得更加明显。

章节回顾

使用本章的策略,我训练了一个 AI 来处理红白骰子。我没有太多耐心,所以我只为一个朋友制作了一个 19x19 的图像。结果相当令人印象深刻。我花了大约 30 分钟将所有的骰子放入图 12-11 中显示的影子盒中。如果没有印刷说明,我想我不会冒这个风险。

19 x 19 成品图像。

图 12-11。完成的 19 x 19 红白骰子带背光

你可以走得更远。哪个疯狂的科学家没有自己的肖像?现在你的肖像可以由骰子制成。也许你可以教一个小机器人如何为你摆放骰子,这样你就可以建造满是数百磅骰子的巨大画框(见图 12-12)。

一个人看着一堵用骰子做成的图像的墙

图 12-12。完美的疯狂科学肖像

你可以继续改进数据并获得更好的结果,你不仅仅局限于普通的黑白骰子。你可以利用你的 AI 技能用装饰性骰子、便利贴、魔方、乐高积木、硬币、木片、饼干、贴纸或其他任何东西来绘画。

虽然这个实验对于 1.0 版本来说是成功的,但我们已经确定了无数个实验,可以让你改进你的模型。

章节挑战:简单如 01、10、11

现在你有了一个强大的新模型,可以成为由黑色0和白色1像素组成的任何照片的艺术家。不幸的是,大多数图像,即使是灰度图像,也有中间值。如果有一种方法可以高效地将图像转换为黑白就好了。

将图像转换为二进制黑白被称为二值化。计算机视觉领域有各种各样的令人印象深刻的算法,可以最好地将图像二值化。让我们专注于最简单的方法。

在这个章节挑战中,使用tf.where方法来检查像素是否超过给定的阈值。使用该阈值,你可以将灰度图像的每个像素转换为10。这将为你的骰子模型准备正常的图形输入。

通过几行代码,你可以将成千上万种光的变化转换为黑白像素,如图 12-13 所示。

一个头骨被转换成黑白像素。

图 12-13。二值化的头骨

你可以在附录 B 中找到这个挑战的答案。

复习问题

让我们回顾一下你在本章编写的代码中学到的知识。花点时间回答以下问题:

  1. TensorFlow.js 的哪个方法允许你将张量分解为一组相等的子张量?

  2. 用于创建数据的稍微修改的替代品以扩大数据集的过程的名称是什么?

  3. 为什么 Gant Laborde 如此了不起?

这些练习的解决方案可以在附录 A 中找到。

¹如果你想了解更多关于暗箱的知识,请观看纪录片Tim's Vermeer

结语

“那么为什么要尝试预测未来,如果这是如此困难,几乎不可能呢?因为做出预测是一种在我们看到自己朝着危险方向漂移时发出警告的方式。因为预测是指出更安全、更明智的途径的有用方式。最重要的是,我们的明天是我们今天的孩子。”

—Octavia E. Butler

构建和撰写这样一个鼓舞人心的框架是一种绝对的乐趣。我简直无法相信我已经在为这本书写后记了,我想您可能对阅读完这本书的感觉也是如此。此刻,我们在这本书中的探索已经结束。然而,您在 TensorFlow.js 中的冒险现在已经开始。

在这本书中,您涵盖了许多机器学习的基础和视觉方面。如果您对机器学习不熟悉,现在您可以深入了解更高级的模型架构,如声音、文本和生成模型。虽然您已经掌握了 TensorFlow.js 的许多基础知识,但还有许多团队与您一起探索着整个可能性的宇宙。

从这里,您可以订阅频道和信息,帮助您成长,连接您所需的人,并将您带入令人惊叹的 TensorFlow.js 项目,您可以在其中构建令人惊叹的产品。

社交

要了解 TensorFlow.js 的最新动态,我强烈建议您进行社交连接。Twitter 标签#MadeWithTFJS经常用于标记 TensorFlow.js 中的新颖和独特的创作。Google 的 TensorFlow.js 社区领导者Jason Mayes在他的展示和讲解活动中帮助推广这个标签,这些活动都在 TensorFlow YouTube 频道上展示。

我强烈建议您在这个频道上与所有过去的演讲者社交,包括本人。社交是一个很好的方式,可以提问,看见想法,并获得进入更多社区的途径。

如果您更喜欢阅读而不是写作,那么连接到 TensorFlow.js 的时代精神仍然很重要。我在https://ai-fyi.com管理一个通讯,我将始终发布 TensorFlow.js 及更多发现的最新和最伟大的内容。

更多书籍

如果您是书籍爱好者,并正在寻找下一个机器学习冒险,那就不要再找了。

*Laurence Maroney(O’Reilly)的《面向程序员的人工智能和机器学习》是一本书,将帮助您将您的 TensorFlow 思维应用到一个新的可能性世界。您将学习如何在各种平台上处理 TensorFlow,以及将您的知识推进到计算机视觉以外的领域。

*Aurélien Géron(O’Reilly)的《使用 Scikit-Learn、Keras 和 TensorFlow 进行实践机器学习》是一个更基础的方法,可以帮助您加强机器学习知识的概念和工具。

*Shanqing Cai 等人(Manning)的《使用 JavaScript 进行深度学习》是有关 TensorFlow.js 和机器学习概念的权威信息来源。

其他选择

在线活动正在飞速增长。搜索您感兴趣的话题的活动,并确保查看 O’Reilly 提供的在线活动。

在线课程是互动培训和认证的绝佳机会。查看 O’Reilly Media 提供的在线课程以及许多作者创建的课程。

如果您正在寻找在 TensorFlow.js 中演讲或咨询,我建议您联系我,我将尽力帮助您联系。

更多 TensorFlow.js 代码

那里有越来越多的优秀 TensorFlow.js 项目。如果您正在寻找灵感,这里有一堆我创建的资源和项目:

谢谢

感谢您,读者。您是这本书存在的原因。请与我分享喜欢的时刻,这样我们可以一起享受。您可以在 Twitter 上找到我,用户名为@GantLaborde,或者访问我的网站GantLaborde.com

附录 A. 章节复习答案

第一章:AI 是魔术

  1. 机器学习是 AI 的一个子集,专注于从数据中学习以提高性能的系统。

  2. 您可以建议获得结果的最佳方法是收集一组带标签的数据,这样您可以执行监督或半监督训练,或者您可以提供无监督或基于强化的方法。

  3. 强化学习最适合将机器学习应用于游戏。

  4. 不,机器学习是人工智能的一个子集。

  5. 不,模型包含结构和数字,但通常比它看到的训练数据小得多。

  6. 数据通常被分成训练集和测试集,有些人使用验证集。训练数据集始终是最大的。

第二章:介绍 TensorFlow.js

  1. 不,TensorFlow 直接与 Python 一起工作。您需要 TensorFlow.js 在浏览器中运行 AI。

  2. 是的,TensorFlow.js 通过 WebGL 可以访问浏览器 GPU,如果加载tensorflow/tfjs-node-gpu,则可以通过 CUDA 访问服务器 GPU。

  3. 不,TensorFlow.js 原始版和 Node.js 版本都不需要 CUDA。

  4. 您将获得该库的最新版本,其中可能包含对您网站的破坏性更改。

  5. 分类器返回一个违规数组及其真实可能性的百分比。

  6. 阈值是可以传递给模型的load调用的可选参数。

  7. 不,毒性模型代码需要模型的网络权重,并且在调用load时会从 TFHub 下载此文件。

  8. 我们不直接进行任何张量操作;库处理所有 JavaScript 原语到张量的转换和反向转换。

第三章:介绍张量

  1. 张量使我们能够以优化的速度处理大量数据和计算,这对于机器学习至关重要。

  2. 没有对象数据类型。

  3. 一个六阶张量将是六阶的。

  4. dataSyncdata都会产生一维类型数组。

  5. 您将收到一个错误。

  6. 张量的size是其形状的乘积,其中rank是张量的形状长度。

    1. 例如,张量tf.tensor([[1,2], [1,2], [1,2]])的形状是[3,2],大小为 6,秩为 2。
  7. 数据类型将是float32

  8. 不,第二个参数是张量的首选形状,不必与输入匹配。

  9. 使用tf.memory().numTensors

  10. 不,tidy必须传递一个普通函数。

  11. 您可以通过使用tf.keep或从封装函数返回张量来保留在tidy内部创建的张量。

  12. 这些值在传统的console.log中不可见,但如果用户使用.print,它们将被记录。

  13. topk函数找到最后一个维度上k个最大条目的值和索引。

  14. 张量被优化用于批量操作。

  15. 有时被称为推荐系统,它是一种寻求预测用户偏好的过滤系统。

第四章:图像张量

  1. 对于值0-255,可以使用int32

  2. tf.tensor([[1, 0, 0],[1, 0, 0]],[[1, 0, 0],[1, 0, 0]])

  3. 一个 50 x 100 的灰度图像,其中 20%是白色。

  4. 错误。3D 张量应该具有大小为 4 的 RGBA 通道,但形状将是三阶的,即[?, ?, 4]

  5. 错误。输出将在输入约束内随机化。

  6. 您可以使用tf.browser.fromPixels

  7. 您将设置值为9

  8. 您可以使用tf.reverse并提供高度轴,如tf.reverse(myImageTensor, 0)

  9. 对于四阶张量进行批处理会更快。

  10. 结果形状将是[20, 20, 3]

第五章:介绍模型

  1. 您可以在 TensorFlow.js 中加载图形和层模型,它们对应的加载方法是tf.loadGraphModeltf.loadLayersModel

  2. 不,JSON 文件知道相应的分片,并且只要有访问权限,它们将被加载。

  3. 您可以从 IndexedDB、本地存储、本地文件系统以及任何其他方式加载模型,以便将它们加载到内存中供 JavaScript 项目使用。

  4. 函数loadLayersModel返回一个解析为模型的 promise。

  5. 可以使用.dispose清除模型。

  6. Inception v3 模型期望一个四维批次,大小为 299 x 299 的 3D RGB 像素,值在 0 到 1 之间。

  7. 您可以使用 2D 上下文的strokeRect方法在画布上绘制边界框。

  8. 第二个参数应该是一个配置对象,带有fromTFHub: true

第六章:高级模型和 UI

  1. SSD 代表“单次检测器”,指的是用于目标检测的完全卷积方法。

  2. 您可以在这些模型上使用executeAsync

  3. SSD MobileNet 模型识别 80 个类别,但每个检测的张量输出形状为 90。

  4. 非极大值抑制(NMS)和软 NMS 用于利用 IoU 去重检测。

  5. 大型同步的 TensorFlow.js 调用可能会记录 UI。通常期望您使用异步或甚至将 TensorFlow.js 后端转换为 CPU,以避免引起 UI 问题。

  6. 画布上下文measureText(label).width测量标签宽度。

  7. globalCompositeOperation设置为source-over将覆盖现有内容。这是绘制到画布的默认设置。

第七章:模型制作资源

  1. 虽然数据量很大,但评估数据的质量和有效特征很重要。一旦数据经过清理并删除了不重要的特征,您可以将其分解为训练、测试和验证集。

  2. 模型过度拟合训练数据,显示出高方差。您应该评估模型在测试集上的表现,并确保它正确学习以便泛化。

  3. 该网站是 Teachable Machine,网址为https://teachablemachine.withgoogle.com

  4. 模型是根据您的特定数据进行训练的,可能不会很好地泛化。您应该使数据集多样化,以避免出现严重偏差。

  5. ImageNet 是用于训练 MobileNet 的数据集。

第八章:训练模型

  1. 本章的训练数据具有一个秩为一且大小为一的输入,输出为秩为一且大小为一。章节挑战要求输入为五个秩为一的输出为四个数字的秩为一张量。

  2. 您可以使用model.summary()查看 Layers 模型的层和可训练参数。

  3. 激活函数创建非线性预测。

  4. 第一个指定的层标识出其所需的inputShape

  5. sgd是一种用于学习的优化方法,代表随机梯度下降。

  6. 一个时期是通过整个训练数据集进行训练的一次迭代。

  7. 描述的模型有一个隐藏层(参见图 A-1)。

描述的模型

图 A-1。一个隐藏层

第九章:分类模型和数据分析

  1. 您将在最后一层使用 softmax,带有三个单元,因为这三个手势是互斥的。

  2. 您将在最后一层使用一个带有 sigmoid 的单个节点/单元。

  3. 您可以通过键入$ dnotebook来运行 Dnotebook。

  4. 您可以使用 Danfo.js 的concat将它们组合,并将它们列在df_list属性中作为数组。

  5. 您将使用 Danfo.js 的get_dummies方法。

  6. 您可以使用dfd.MinMaxScaler()来缩放您的模型。

第十章:图像训练

  1. 卷积层有许多可训练的滤波器。

  2. 卷积窗口大小为kernelSize

  3. 为了保持卷积结果的大小不变,您需要通过将层的padding属性设置为'same'来填充卷积。

  4. 错误。卷积层可以处理多维输入。在将它们连接到密集神经网络之前,您必须展平一组卷积的输出。

  5. 一个 3 x 3 的步幅为三的卷积会将每个维度减少三分之一。因此,结果图像将变为更小的 27 x 27。

  6. 不,你需要知道 12 以外存在多少可能的值,这样函数才能添加所需的零。

第十一章:迁移学习

  1. KNN 代表 K-最近邻算法。

  2. 即使使用迁移学习,小数据集也容易过拟合或具有高方差。

  3. 图像特征向量标记模型是经过训练的卷积。

  4. 1.00 将比 0.50 具有 2 倍的特征。

  5. 将第二个参数设置为true.infer方法将返回嵌入。

  6. 你已经添加到已经训练模型的初始层训练得非常差,你应该确保在训练新层时不要修改已经训练好的层。一切就绪后,你可以结合并进行“微调”训练。

  7. 你应该将输入数据展平,以便后续网络的密集层能够正确处理。

第十二章:Dicify:毕业项目

  1. 你可以使用tf.split将张量沿着给定轴分割成相等的子张量。

  2. 这个过程被称为数据增强

  3. 科学家们多年来一直在研究这个问题,虽然尚未确定来源,但它已被普遍接受为科学事实。

附录 B. 章节挑战答案

第二章:卡车警报!

MobileNet 模型可以检测各种不同类型的卡车。您可以通过查看可识别卡车的列表来解决这个问题,或者您可以简单地在给定的类名列表中搜索truck这个词。为简单起见,提供的答案选择了后者。

包含 HTML 和 JavaScript 的整个解决方案在这里:

<!DOCTYPE html>
<html>
  <head>
    <script
    src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.7.0/dist/tf.min.js">
  </script>
    <script
    src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@1.0.0">
  </script> <!-- ① -->
    <script>
      mobilenet.load().then(model => {
        const img = document.getElementById('myImage'); <!-- ② -->
        // Classify the image
        model.classify(img).then(predictions => {
          console.log('Predictions: ', predictions);
          // Was there a truck?
          let foundATruck
          predictions.forEach(p => {
            foundATruck = foundATruck || p.className.includes("truck") <!-- ③ -->
          })
          // TRUCK ALERT!
          if (foundATruck) alert("TRUCK DETECTED!") <!-- ④ -->
        });
      });
    </script>
  </head>
  <body>
    <h1>Is this a truck?</h1>
    <img id="myImage" src="truck.jpg" width="100%"></img>
  </body>
</html>

从 CDN 加载 MobileNet 模型。

通过 ID 访问 DOM 上的图像。由于等待模型加载,DOM 可能已经加载了一段时间。

如果在任何预测中检测到truck这个词,将foundATruck设置为 true。

真相时刻!只有在foundATruck为 true 时才会弹出警报。

这个带有卡车图像的章节挑战答案可以在本书的GitHub源代码中找到。

第三章:你有什么特别之处?

这个简单的练习是关于查找 TensorFlow.js tf.unique方法。一旦找到这个友好的方法,就很容易构建一个解决方案,就像这样:

const callMeMaybe = tf.tensor([8367677, 4209111, 4209111, 8675309, 8367677])
const uniqueTensor = tf.unique(callMeMaybe).values
const result = uniqueTensor.arraySync()
console.log(`There are ${result.length} unique values`, result)

不要忘记将此代码包装在tf.tidy中以进行自动张量清理!

第四章:混乱排序

一种优雅的解决方案是对randomUniform创建的张量使用topk进行排序。由于randomUniform创建的值在01之间,并且topk沿着最后一个轴对值进行排序,您可以使用以下代码完成这个练习:

const rando = tf.randomUniform([400, 400]) // ①
const sorted = tf.topk(rando, 400).values // ②
const answer = sorted.reshape([400, 400, 1]) // ③

创建一个 2D 的 400 x 400 张量,其中包含介于01之间的随机值。

使用topk对最后一个维度(宽度)进行排序,并返回所有 400 个值。

可选:将张量重塑为 3D 值。

先前的解决方案非常冗长,可以压缩为一行代码:

tf.topk(tf.randomUniform([400, 400]), 400).values

第五章:可爱的脸

现在,第一个模型已经给出了脸部的坐标,一个张量裁剪将提供这些像素。这几乎与strokeRect完全相同,因为您提供了一个起始位置和所需的大小。然而,我们之前的所有测量对于这个裁剪都不起作用,因为它们是在图像的调整版本上计算的。您需要在原始张量数据上进行类似的计算,以便提取正确的信息。

提示

如果您不想重新计算位置,可以将张量调整大小以匹配petImage的宽度和高度。这将允许您重用相同的startXstartYwidthheight变量进行裁剪。

以下代码可能引用原始人脸定位代码中创建的一些变量,特别是原始的fromPixels张量myTensor

// Same bounding calculations but for the tensor
const tHeight = myTensor.shape[0] // ①
const tWidth = myTensor.shape[1]
const tStartX = box[0] * tWidth
const tStartY = box[1] * tHeight
const cropLength = parseInt((box[2] - box[0]) * tWidth, 0)  // ②
const cropHeight = parseInt((box[3] - box[1]) * tHeight, 0)

const startPos = [tStartY, tStartX, 0]
const cropSize = [cropHeight, cropLength, 3]

const cropped = tf.slice(myTensor, startPos, cropSize)

// Prepare for next model input
const readyFace = tf.image
  .resizeBilinear(cropped, [96, 96], true)
  .reshape([1, 96, 96, 3]);  // ③

请注意,张量的顺序是高度然后宽度。它们的格式类似于数学矩阵,而不是图像特定的宽度乘以高度的标准。

减去比率可能会留下浮点值;您需要将这些值四舍五入到特定的像素索引。在这种情况下,答案是使用parseInt来去除任何小数。

显然,批处理,然后取消批处理,然后重新批处理是低效的。在可能的情况下,您应该将所有操作保持批处理,直到绝对必要。

现在,您已经成功地准备好将狗脸张量传递到下一个模型中,该模型将返回狗在喘气的可能性百分比。

结果模型的输出从未指定,但您可以确保它将是一个两值的一维张量,索引 0 表示不 panting,索引 1 表示 panting,或者是一个一值的一维张量,表示从零到一的 panting 可能性。这两种情况都很容易处理!

第六章:顶级侦探

使用topk的问题在于它仅在特定张量的最终维度上起作用。因此,您可以通过两次调用topk来找到两个维度上的最大值。第二次您可以将结果限制为前三名。

const { indices, values } = tf.topk(t)
const topvals = values.squeeze()
const sorted = tf.topk(topvals, 3)
// prints [3, 4, 2]
sorted.indices.print()

然后,您可以循环遍历结果并从topvals变量中访问前几个值。

第七章:再见,MNIST

通过向导您可以选择所有所需的设置;您应该已经创建了一些有趣的结果。结果应该如下:

  • 100 个二进制文件被生成在一个分组中。

  • 最终大小约为 1.5 MB。

  • 由于大小为 1.5 MB,如果使用默认值,这可以适合单个 4 MB 分片。

第八章:模型架构师

您被要求创建一个符合给定规格的 Layers 模型。该模型的输入形状为五,输出形状为四,中间有几个具有指定激活函数的层。

构建模型的代码应该如下所示:

const model = tf.sequential();

model.add(
  tf.layers.dense({
    inputShape: 5,
    units: 10,
    activation: "sigmoid"
  })
);

model.add(
  tf.layers.dense({
    units: 7,
    activation: "relu"
  })
);

model.add(
  tf.layers.dense({
    units: 4,
    activation: "softmax"
  })
);

model.compile({
  optimizer: "adam",
  loss: "categoricalCrossentropy"
});

可训练参数的数量计算为进入一个层的行数 + 该层中的单元数。您可以使用每个层的计算layerUnits[i] * layerUnits[i - 1] + layerUnits[i]来解决这个问题。model.summary()的输出将验证您的数学计算。将您的摘要与示例 B-1 进行比较。

示例 B-1. 模型摘要
_________________________________________________________________
Layer (type)                 Output shape              Param #
=================================================================
dense_Dense33 (Dense)        [null,10]                 60
_________________________________________________________________
dense_Dense34 (Dense)        [null,7]                  77
_________________________________________________________________
dense_Dense35 (Dense)        [null,4]                  32
=================================================================
Total params: 169
Trainable params: 169
Non-trainable params: 0
_________________________________________________________________

第九章:船出事了

当然,有很多获取这些信息的方法。这只是其中一种方式。

要提取每个名称的敬语,您可以使用.apply并通过空格分割。这将让您很快得到大部分答案。但是,一些名称中有“von”之类的内容,这会导致额外的空格并稍微破坏您的代码。为此,一个好的技巧是使用正则表达式。我使用了/,\s(.*?)\./,它查找逗号后跟一个空格,然后匹配直到第一个句点。

您可以应用这个方法创建一个新行,按该行分组,然后使用.mean()对幸存者的平均值进行表格化。

mega_df['Name'] = mega_df['Name'].apply((x) => x.split(/,\s(.*?)\./)[1])
grp = mega_df.groupby(['Name'])
table(grp.col(['Survived']).mean())

mega_df['Name']被替换为有用的内容,然后进行分组以进行验证。然后可以轻松地对其进行编码或进行分箱处理以用于您的模型。

图 B-1 显示了在 Dnotebook 中显示的分组代码的结果。

Dnotebook 解决方案的屏幕截图

图 B-1. 敬语和生存平均值

第十章:保存魔法

为了保存最高的验证准确性,而不是最后的验证准确性,您可以在时期结束回调中添加一个条件保存。这可以避免您意外地陷入过拟合时期的困扰。

// initialize best at zero
let best = 0

//...

// In the callback object add the onEpochEnd save condition
onEpochEnd: async (_epoch, logs) => {
  if (logs.val_acc > best) {
    console.log("SAVING")
    model.save(savePath)
    best = logs.val_acc
  }
}

还有earlyStopping预打包回调,用于监视和防止过拟合。将您的回调设置为callbacks: tf.callbacks.earlyStopping({monitor: 'val_acc'})将在验证准确性回退时停止训练。

第十一章:光速学习

您现在知道很多解决这个问题的方法,但我们将采取快速简单的方式。解决这个问题有四个步骤:

  1. 加载新的图像数据

  2. 将基础模型削减为特征模型

  3. 创建读取特征的新层

  4. 训练新层

加载新的图像数据:

const dfy = await dfd.read_csv('labels.csv')
const dfx = await dfd.read_csv('images.csv')

const Y = dfy.tensor
const X = dfx.tensor.reshape([dfx.shape[0], 28, 28, 1])

将基础模型削减为特征模型:

const model = await tf.loadLayersModel('sorting_hat/model.json')
const layer = model.getLayer('max_pooling2d_MaxPooling2D3')
const shaved = tf.model({
  inputs: model.inputs,
  outputs: layer.output
})
// Run data through shaved model to get features
const XFEATURES = shaved.predict(X)

创建读取特征的新层:

transferModel = tf.sequential({
  layers: [
    tf.layers.flatten({ inputShape: shaved.outputs[0].shape.slice(1) }),
    tf.layers.dense({ units: 128, activation: 'relu' }),
    tf.layers.dense({ units: 3, activation: 'softmax' }),
  ],
})
transferModel.compile({
  optimizer: 'adam',
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
})

训练新层:

await transferModel.fit(XFEATURES, Y, {
  epochs: 10,
  validationSplit: 0.1,
  callbacks: {
    onEpochEnd: console.log,
  },
})

结果在 10 个时期内训练到了很高的准确性,如图 B-2 所示。

在几个时期内达到完美的验证准确性

图 B-2. 仅从 150 张图像训练

这个挑战的完整答案可以在本章的相关源代码中找到,这样你就可以查看代码,甚至与结果进行交互。

第十二章:简单如 01, 10, 11

将图像转换为灰度很容易。一旦你这样做了,你可以在图像上使用 tf.where 来用白色或黑色像素替换每个像素。

以下代码将具有 input ID 的图像转换为一个二值化图像,该图像显示在同一页上名为 output 的画布上:

// Simply read from the DOM
const inputImage = document.getElementById('input')
const inTensor = tf.browser.fromPixels(inputImage, 1)

// Binarize
const threshold = 50
const light = tf.onesLike(inTensor).asType('float32')
const dark = tf.zerosLike(inTensor)
const simpleBinarized = tf.where(
  tf.less(inTensor, threshold),
  dark, // False Case: place zero
  light, // True Case: place one
)

// Show results
const myCanvas = document.getElementById('output')
tf.browser.toPixels(simpleBinarized, myCanvas)

本章挑战答案的完全运行示例可以在本章的相关源代码中找到。

有更高级和更健壮的方法来对图像进行二值化。如果你想处理更多的图像,请查看二值化算法。

附录 C. 权利和许可

Unsplash 许可

Unsplash 授予您不可撤销的、非独占的、全球性的版权许可,允许您免费下载、复制、修改、分发、执行和使用 Unsplash 的照片,包括商业用途,无需征得摄影师或 Unsplash 的许可。该许可不包括从 Unsplash 编译照片以复制类似或竞争性服务的权利。

在此许可下的图像:

第二章

图 2-5:Milovan Vudrag 拍摄的照片

第五章

图 5-9:Karsten Winegeart 拍摄的照片

图 5-4:Dave Weatherall 拍摄的照片

第六章

图 6-15:Kelsey Chance 拍摄的照片

第十一章

图 11-2,骆驼:Wolfgang Hasselmann 拍摄的照片

图 11-2,天竺鼠:Jack Catalano 拍摄的照片

图 11-2,水豚:Dušan Veverkolog 拍摄的照片

图 11-8,兔子 1:Satyabrata sm 拍摄的照片

图 11-8,兔子 2:Gary Bendig 拍摄的照片

图 11-8,兔子 3:Gavin Allanwood 拍摄的照片

图 11-8,汽车 1:Sam Pearce-Warrilow 拍摄的照片

图 11-8,汽车 2:Cory Rogers 拍摄的照片

图 11-8,汽车 3:Kevin Bhagat 拍摄的照片

图 11-8,测试兔子:Christopher Paul High 拍摄的照片

第十二章

图 12-12,修改后的 Igor Miske 拍摄的照片

图 12-13:Gant Laborde 拍摄的照片

Apache 许可证 2.0

版权所有 2017 © Google

根据 Apache 许可证第 2.0 版(“许可证”)许可,除非符合许可证的规定,否则您不得使用此文件。您可以在http://www.apache.org/licenses/LICENSE-2.0获取许可证的副本。

除非适用法律要求或书面同意,根据许可证分发的软件是基于“原样”分发的,没有任何明示或暗示的担保或条件。请查看许可证以了解许可证下的权限和限制的具体语言。

在此许可下的图像:

在此许可下的代码:

  • 第二章:毒性模型 NPM

  • 第二章:MobileNet 模型 NPM

  • 第五章:Inception v3 模型

公共领域

在此许可下的图像:

WTFPL

根据此许可的数据](http://www.wtfpl.net):

  • 第五章:井字游戏模型

  • 第五章:宠物面孔模型

  • 第十章:分拣帽模型

知识共享署名-相同方式共享 4.0 国际许可协议(CC BY-SA 4.0)

根据此许可的数据](https://creativecommons.org/licenses/by-sa/4.0):

在此许可下的图像:

知识共享署名 4.0 国际许可协议(CC BY 4.0)

根据此许可证的数据:

Gant Laborde 和 O’Reilly

除了在附录 C 中明确标识的图像外,所有其他图像均由 O’Reilly 或作者 Gant Laborde 拥有,用于此出版作品的明确使用。

TensorFlow 和 TensorFlow.js 标志

TensorFlow、TensorFlow 标志和任何相关标记均为 Google Inc. 的商标。

posted @ 2025-11-19 09:22  绝不原创的飞龙  阅读(14)  评论(0)    收藏  举报