tinyml-ml-tf-merge-0

244 阅读1小时+

Tinyml:TensorFlow Lite 深度学习(一)

原文:Tinyml: Machine Learning with Tensorflow Lite

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

长久以来,电子产品一直吸引着我的想象力。我们学会了从地球中挖掘岩石,以神秘的方式对其进行精炼,并生产出令人眼花缭乱的微小组件,我们根据神秘的法则将它们组合在一起,赋予它们一些生命的本质。

在我八岁的时候,电池、开关和灯丝灯已经足够迷人了,更不用说我家里家用电脑内部的处理器了。随着岁月的流逝,我对电子和软件原理有了一些了解,使这些发明能够运行。但总让我印象深刻的是,一系列简单的元素如何组合在一起创造出一个微妙而复杂的东西,而深度学习真的将这一点推向了新的高度。

这本书的一个例子是一个深度学习网络,从某种意义上说,它懂得如何看。它由成千上万个虚拟的“神经元”组成,每个神经元都遵循一些简单的规则并输出一个数字。单独来看,每个神经元并不能做太多事情,但是结合起来,并且通过训练,给予一点人类知识,它们可以理解我们复杂的世界。

这个想法中有一些魔力:简单的算法在由沙子、金属和塑料制成的微型计算机上运行,可以体现出人类理解的一部分。这就是 TinyML 的本质,这是 Pete 创造的一个术语,将在第一章中介绍。在本书的页面中,您将找到构建这些东西所需的工具。

感谢您成为我们的读者。这是一个复杂的主题,但我们努力保持简单并解释您需要的所有概念。我们希望您喜欢我们所写的内容,我们很期待看到您创造的东西!

Daniel Situnayake

本书中使用的约定

本书中使用了以下排版约定:

斜体

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

常量宽度

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

常量宽度粗体

显示用户应该按原样输入的命令或其他文本。

常量宽度斜体

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

提示

这个元素表示提示或建议。

注意

这个元素表示一般注释。

警告

这个元素表示警告或注意。

使用代码示例

补充材料(代码示例、练习等)可在https://tinymlbook.com/supplemental下载。

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

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

我们感谢,但通常不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“TinyML by Pete Warden and Daniel Situnayake (O’Reilly). Copyright Pete Warden and Daniel Situnayake, 978-1-492-05204-3.”

如果您觉得您对代码示例的使用超出了合理使用范围或上述给出的许可,请随时联系我们,邮箱为permissions@oreilly.com

致谢

我们要特别感谢 Nicole Tache 出色的编辑工作,Jennifer Wang 的启发性魔杖示例,以及 Neil Tan 在 uTensor 库中进行的开创性嵌入式 ML 工作。没有 Rajat Monga 和 Sarah Sirajuddin 的专业支持,我们无法完成这本书的写作。我们还要感谢我们的合作伙伴 Joanne Ladolcetta 和 Lauren Ward 的耐心。

这本书是来自硬件、软件和研究领域数百人的努力成果,特别是来自 TensorFlow 团队。虽然我们只能提及一部分人,对于我们遗漏的每个人表示歉意,我们要感谢:Mehmet Ali Anil,Alasdair Allan,Raziel Alvarez,Paige Bailey,Massimo Banzi,Raj Batra,Mary Bennion,Jeff Bier,Lukas Biewald,Ian Bratt,Laurence Campbell,Andrew Cavanaugh,Lawrence Chan,Vikas Chandra,Marcus Chang,Tony Chiang,Aakanksha Chowdhery,Rod Crawford,Robert David,Tim Davis,Hongyang Deng,Wolff Dobson,Jared Duke,Jens Elofsson,Johan Euphrosine,Martino Facchin,Limor Fried,Nupur Garg,Nicholas Gillian,Evgeni Gousev,Alessandro Grande,Song Han,Justin Hong,Sara Hooker,Andrew Howard,Magnus Hyttsten,Advait Jain,Nat Jeffries,Michael Jones,Mat Kelcey,Kurt Keutzer,Fredrik Knutsson,Nick Kreeger,Nic Lane,Shuangfeng Li,Mike Liang,Yu-Cheng Ling,Renjie Liu,Mike Loukides,Owen Lyke,Cristian Maglie,Bill Mark,Matthew Mattina,Sandeep Mistry,Amit Mittra,Laurence Moroney,Boris Murmann,Ian Nappier,Meghna Natraj,Ben Nuttall,Dominic Pajak,Dave Patterson,Dario Pennisi,Jahnell Pereira,Raaj Prasad,Frederic Rechtenstein,Vikas Reddi,Rocky Rhodes,David Rim,Kazunori Sato,Nathan Seidle,Andrew Selle,Arpit Shah,Marcus Shawcroft,Zach Shelby,Suharsh Sivakumar,Ravishankar Sivalingam,Rex St. John,Dominic Symes,Olivier Temam,Phillip Torrone,Stephan Uphoff,Eben Upton,Lu Wang,Tiezhen Wang,Paul Whatmough,Tom White,Edd Wilder-James 和 Wei Xiao。

第一章:介绍

本书的目标是展示任何具有基本命令行终端和代码编辑器使用经验的开发人员如何开始构建自己的项目,运行嵌入式设备上的机器学习(ML)。

当我 2014 年首次加入谷歌时,我发现了许多我之前不知道存在的内部项目,但最令人兴奋的是 OK Google 团队正在进行的工作。他们运行的神经网络只有 14 千字节(KB)!它们需要如此小是因为它们在大多数 Android 手机中的数字信号处理器(DSP)上运行,持续监听“OK Google”唤醒词,而这些 DSP 只有几十 KB 的 RAM 和闪存。团队必须使用 DSP 来完成这项工作,因为主 CPU 已关闭以节省电池,而这些专用芯片只使用几毫瓦(mW)的功率。

从深度学习的图像方面来看,我从未见过如此小的网络,以及使用低功耗芯片来运行神经模型的想法一直留在我心中。当我努力让 TensorFlow 和后来的 TensorFlow Lite 在 Android 和 iOS 设备上运行时,我仍然被与简单芯片合作的可能性所吸引。我了解到在音频领域还有其他开创性的项目(如 Pixel 的 Music IQ)用于预测性维护(如 PsiKick),甚至在视觉领域(高通的 Glance 相机模块)也有类似的项目。

对我来说,很明显有一类全新的产品正在涌现,其关键特征是它们利用机器学习来理解嘈杂的传感器数据,可以使用电池或能量收集器运行多年,成本仅为一两美元。我反复听到的一个术语是“剥离和粘贴传感器”,用于不需要更换电池的设备,可以应用于环境中的任何地方并被遗忘。要使这些产品变为现实,需要将原始传感器数据转化为可操作信息的方式,本地在设备上进行处理,因为传输数据流的能量成本被证明太高,以至于不切实际。

这就是 TinyML 的概念所在。与行业和学术界同事长时间的交谈导致了一个粗略的共识,即如果你能以低于 1 毫瓦的能量成本运行神经网络模型,那么将会有许多全新的应用变得可能。这个数字可能看起来有点随意,但如果你将其转化为具体的术语,那就意味着一个运行在硬币电池上的设备可以使用一年。这将导致一个产品足够小,可以适应任何环境,并能够在没有任何人为干预的情况下运行一段有用的时间。

我将直接使用一些技术术语来讨论本书将涵盖的内容,但如果其中一些对您不熟悉,不要担心;我们在第一次使用它们时会定义它们的含义。

此时,你可能会想到像树莓派或 NVIDIA 的 Jetson 开发板这样的平台。这些设备非常棒,我自己经常使用,但即使是最小的树莓派也类似于手机的主 CPU,因此需要数百毫瓦的电力。即使只是让它运行几天,也需要类似于智能手机的电池,这使得构建真正无线的体验变得困难。NVIDIA 的 Jetson 基于强大的 GPU,当以全速运行时,我们看到它使用了高达 12 瓦的功率,因此即使没有大型外部电源供应,也更难以使用。这通常在汽车或机器人应用中不是问题,因为机械部件本身需要大功率源,但这确实使得在我最感兴趣的那些需要在没有有线电源的情况下运行的产品上使用这些平台变得困难。幸运的是,使用它们时,由于缺乏资源限制,通常可以使用像 TensorFlow、TensorFlow Lite 和 NVIDIA 的 TensorRT 这样的框架,因为它们通常基于 Linux 兼容的 Arm Cortex-A CPU,具有数百兆字节的内存。本书不会专注于描述如何在这些平台上运行,原因就是刚才提到的,但如果你感兴趣,有很多资源和文档可用;例如,请参阅TensorFlow Lite 的移动文档

我关心的另一个特征是成本。最便宜的树莓派 Zero 面向制造商的价格是 5 美元,但很难以那个价格大量购买这类芯片。Zero 的购买通常受到数量限制,而工业购买的价格并不透明,但很明显 5 美元绝对是不寻常的。相比之下,最便宜的 32 位微控制器每个成本远低于一美元。这种低价使得制造商能够用软件定义的替代方案替换传统的模拟或电机控制电路,从玩具到洗衣机的一切。我希望我们可以利用这些设备中微控制器的普及性,通过软件更新引入人工智能,而无需对现有设计进行大量更改。这也应该使得能够在建筑物或野生动物保护区等环境中部署大量智能传感器,而不会使成本超过收益或可用资金。

嵌入式设备

TinyML 的定义是指能耗低于 1 毫瓦,这意味着我们需要寻找嵌入式设备作为硬件平台。直到几年前,我自己对它们并不熟悉——它们对我来说充满了神秘感。传统上它们是 8 位设备,使用晦涩和专有的工具链,因此开始使用任何一个都显得非常令人生畏。一个重要的进步是当 Arduino 推出了用户友好的集成开发环境(IDE)以及标准化的硬件。从那时起,32 位 CPU 已经成为标准,这在很大程度上要归功于 Arm 的 Cortex-M 系列芯片。几年前我开始原型设计一些机器学习实验时,我惊讶地发现开发过程变得相对简单。

嵌入式设备仍然受到一些严格的资源限制。它们通常只有几百千字节的 RAM,有时甚至更少,并且具有类似数量的闪存用于持久性程序和数据存储。时钟速度只有几十兆赫是很常见的。它们肯定不会有完整的 Linux(因为那需要内存控制器和至少一兆字节的 RAM),如果有操作系统,很可能不会提供您期望的所有或任何 POSIX 或标准 C 库函数。许多嵌入式系统避免使用像newmalloc()这样的动态内存分配函数,因为它们被设计为可靠且长时间运行,如果有一个可以被碎片化的堆,要确保这一点是极其困难的。您可能还会发现使用调试器或其他来自桌面开发的熟悉工具会有些棘手,因为您将使用的接口非常专业化。

然而,当我学习嵌入式开发时,也有一些令人惊喜的地方。拥有没有其他进程来中断您的程序的系统可以使构建对发生的事情的心理模型变得非常简单,而没有分支预测或指令流水线的处理器的直接性质使手动汇编优化比在更复杂的 CPU 上更容易。我也发现,在一个可以平衡在指尖上的微型计算机上看到 LED 灯亮起,知道它每秒运行数百万条指令来理解周围世界,这带来了一种简单的喜悦。

变化的景观

直到最近,我们才能在微控制器上运行机器学习,这个领域非常年轻,这意味着硬件、软件和研究都在非常快速地变化。这本书基于 2019 年的世界快照,这一领域意味着一些部分在我们完成最后一章的写作之前就已经过时了。我们努力确保我们依赖的硬件平台将长期可用,但设备很可能会继续改进和演变。我们使用的 TensorFlow Lite 软件框架具有稳定的 API,我们将继续支持文本中提供的示例,但我们还提供了所有示例代码和文档的最新版本的网页链接。例如,您可以期望看到覆盖更多用例的参考应用程序被添加到 TensorFlow 存储库中。我们还致力于专注于调试、模型创建和开发对深度学习工作原理的理解等技能,即使您使用的基础设施发生变化,这些技能也将保持有用。

我们希望这本书能为您提供开发嵌入式机器学习产品所需的基础,以解决您关心的问题。希望我们能够帮助您开始建立一些令人兴奋的新应用程序,我相信在未来几年内这个领域将涌现出一些新的应用程序。

皮特·沃登

第二章:入门

在本章中,我们将介绍如何开始在低功耗设备上构建和修改机器学习应用程序所需的知识。所有软件都是免费的,硬件开发套件的价格不到 30 美元,因此最大的挑战可能是开发环境的陌生。为了帮助解决这个问题,在整个章节中,我们推荐了一套我们发现可以很好地配合使用的工具。

这本书的目标读者是谁?

要构建一个 TinyML 项目,您需要了解一些机器学习和嵌入式软件开发的知识。这两者都不是常见的技能,很少有人是这两者的专家,因此本书将假设您对这两者都没有背景。唯一的要求是您对在终端中运行命令(或 Windows 上的命令提示符)有一定的熟悉度,并且能够将程序源文件加载到编辑器中,进行修改并保存。即使听起来令人生畏,我们会逐步引导您完成我们讨论的每一步,就像一个好的食谱一样,包括许多情况下的截图(和在线屏幕录像),因此我们希望尽可能地使这本书对广大读者更易接近。

我们将向您展示如何在嵌入式设备上应用机器学习的一些实际应用,例如简单的语音识别、使用运动传感器检测手势以及使用摄像头传感器检测人员。我们希望让您熟悉自己构建这些程序,然后扩展它们以解决您关心的问题。例如,您可能想修改语音识别以检测狗吠声而不是人类讲话,或者识别狗而不是人类,我们会给您一些关于如何自行解决这些修改的想法。我们的目标是为您提供开始构建您关心的令人兴奋的应用程序所需的工具。

需要哪些硬件?

您需要一台带有 USB 端口的笔记本电脑或台式电脑。这将是您的主要编程环境,您将在其中编辑和编译在嵌入式设备上运行的程序。您将使用 USB 端口和一个专用适配器将此计算机连接到嵌入式设备,具体取决于您使用的开发硬件。主计算机可以运行 Windows、Linux 或 macOS。对于大多数示例,我们在云中训练我们的机器学习模型,使用Google Colab,因此不用担心是否拥有专门配备的计算机。

您还需要一个嵌入式开发板来测试您的程序。要做一些有趣的事情,您需要连接麦克风、加速度计或摄像头,并且您需要一个足够小的电池,可以构建成一个逼真的原型项目。当我们开始写这本书时,这是很困难的,所以我们与芯片制造商 Ambiq 和创客零售商 SparkFun 合作,生产了价值 15 美元的 SparkFun Edge 板。本书的所有示例都可以在此设备上运行。

提示

SparkFun Edge 板的第二次修订版,SparkFun Edge 2,在这本书出版后将发布。本书中的所有项目都保证可以与新板卡配合使用。然而,这里打印的代码和部署说明将略有不同。不用担心,每个项目章节都链接到一个包含部署每个示例到 SparkFun Edge 2 的最新说明的README.md

我们还提供了如何在 Arduino 和 Mbed 开发环境中运行许多项目的说明。我们推荐使用Arduino Nano 33 BLE Sense板和Mbed 的 STM32F746G Discovery kit开发板,尽管所有项目都应该适用于其他设备,只要您能够以所需格式捕获传感器数据。表 2-1 显示了我们在每个项目章节中包含的设备。

表 2-1。每个项目中涉及的设备

项目名称章节SparkFun EdgeArduino Nano 33 BLE SenseSTM32F746G Discovery kit
Hello world第五章包括包括包括
唤醒词检测第七章包括包括包括
人员检测第九章包括包括不包括
魔杖第十一章包括包括不包括

除了人员检测需要相机模块外,这些项目都不需要任何额外的电子组件。如果您使用的是 Arduino,您将需要Arducam Mini 2MP Plus。如果您使用的是 SparkFun Edge,您将需要 SparkFun 的Himax HM01B0 breakout

您需要哪些软件?

本书中的所有项目都基于 TensorFlow Lite for Microcontrollers 框架。这是 TensorFlow Lite 框架的一个变体,旨在在仅有几十千字节可用内存的嵌入式设备上运行。所有这些项目都作为库中的示例包含在内,它是开源的,您可以在GitHub上找到它。

注意

由于本书中的代码示例是一个活跃的开源项目的一部分,随着我们添加优化、修复错误和支持其他设备,它们将不断变化和发展。您可能会发现书中打印的代码与 TensorFlow 存储库中最新代码之间存在一些差异。尽管代码可能随着时间的推移而有所变化,但您在这里学到的基本原则将保持不变。

您需要某种编辑器来检查和修改您的代码。如果您不确定应该使用哪种编辑器,微软的免费VS Code 应用程序是一个很好的起点。它适用于 macOS、Linux 和 Windows,并具有许多方便的功能,如语法高亮和自动完成。如果您已经有喜欢的编辑器,可以使用它,我们不会为任何项目进行大量修改。

您还需要一个输入命令的地方。在 macOS 和 Linux 上,这被称为终端,您可以在应用程序文件夹中找到它。在 Windows 上,它被称为命令提示符,您可以在开始菜单中找到它。

还将有额外的软件,您需要与嵌入式开发板通信,但这将取决于您使用的设备。如果您使用的是 SparkFun Edge 开发板或 Mbed 设备,您需要安装 Python 用于一些构建脚本,然后您可以在 Linux 或 macOS 上使用 GNU Screen,或者在 Windows 上使用Tera Term来访问调试日志控制台,显示来自嵌入式设备的文本输出。如果您有 Arduino 开发板,您所需的一切都已安装在 IDE 中,因此您只需要下载主要软件包。

我们希望您学到什么?

本书的目标是帮助更多应用程序在这个新领域中出现。目前没有一个“杀手级应用程序”适用于 TinyML,也许永远不会有,但我们从经验中知道,世界上有很多问题可以通过它提供的工具箱来解决。我们希望让您熟悉可能的解决方案。我们希望带领农业、空间探索、医学、消费品等领域的专家了解如何自己解决问题,或者至少了解这些技术可以解决哪些问题。

考虑到这一点,我们希望当您完成这本书时,您将对目前嵌入式系统上使用机器学习的可能性有一个良好的概述,同时也对未来几年可能实现的可能性有一些想法。我们希望您能够构建和修改一些使用时间序列数据(如音频或加速度计输入)以及低功耗视觉的实际示例。我们希望您对整个系统有足够的理解,至少能够有意义地参与与专家讨论新产品设计,并希望能够自己原型早期版本。

由于我们希望看到完整的产品问世,我们从整个系统的角度来看待我们讨论的一切。通常,硬件供应商会关注他们正在销售的特定组件的能耗,但不考虑其他必要部分如何增加所需的功率。例如,如果您有一个只消耗 1 mW 的微控制器,但它所使用的唯一摄像头传感器需要 10 mW 才能运行,那么您用它的任何基于视觉的产品都无法利用处理器的低能耗。这意味着我们不会深入研究不同领域的基本工作原理;相反,我们专注于您需要了解的内容,以便使用和修改涉及的组件。

例如,当您在 TensorFlow 中训练模型时,我们不会详细讨论发生在幕后的细节,比如梯度和反向传播的工作原理。相反,我们向您展示如何从头开始运行训练以创建模型,您可能会遇到的常见错误以及如何处理它们,以及如何定制流程来构建模型以解决您自己的问题与新数据集。

第三章:快速了解机器学习

在技术领域中,很少有像机器学习和人工智能(AI)周围那样神秘的领域。即使您是另一个领域的经验丰富的工程师,机器学习也可能看起来是一个需要大量先验知识的复杂主题。许多开发人员在开始阅读有关机器学习的内容时会感到沮丧,因为这些解释涉及学术论文、晦涩的 Python 库和高级数学。甚至知道从哪里开始都可能感到令人生畏。

实际上,机器学习很容易理解,任何人都可以通过文本编辑器访问。学习了一些关键思想后,您可以轻松地在自己的项目中使用它。在所有神秘感之下,是一套解决各种问题的有用工具。有时候可能会感觉像魔术,但其实只是代码,您不需要博士学位来使用它。

这本书是关于如何在微型设备上使用机器学习的。在本章的其余部分,您将学习所有开始所需的机器学习知识。我们将涵盖基本概念,探索一些工具,并训练一个简单的机器学习模型。我们的重点是微型硬件,因此我们不会花太多时间讨论深度学习背后的理论,或者使其运作的数学。后面的章节将更深入地探讨工具和如何优化嵌入式设备的模型。但是在本章结束时,您将熟悉关键术语,了解一般工作流程,并知道去哪里学习更多。

在本章中,我们涵盖以下内容:

  • 机器学习实际上是什么

  • 它可以解决的问题类型

  • 关键术语和思想

  • 使用深度学习解决问题的工作流程,这是机器学习中最流行的方法之一

提示

有许多书籍和课程解释深度学习背后的科学,所以我们不会在这里做这个。尽管如此,这是一个迷人的主题,我们鼓励您去探索!我们在“学习机器学习”中列出了一些我们喜欢的资源。但请记住,您不需要所有的理论来开始构建有用的东西。

机器学习实际上是什么

想象您拥有一台制造小部件的机器。有时它会出故障,修复起来很昂贵。也许如果您在机器运行期间收集数据,您可能能够预测何时会出现故障,并在损坏发生之前停止运行。例如,您可以记录其生产速率、温度和振动情况。也许这些因素的某种组合表明即将出现问题。但是您如何找出呢?

这是机器学习旨在解决的问题类型的示例。从根本上讲,机器学习是一种利用计算机根据过去观察来预测事物的技术。我们收集有关工厂机器性能的数据,然后创建一个计算机程序来分析这些数据,并用它来预测未来状态。

创建机器学习程序与编写代码的传统过程不同。在传统软件中,程序员设计一个算法,该算法接受输入,应用各种规则,并返回输出。程序员计划算法的内部操作,并通过代码行明确实现。要预测工厂机器的故障,程序员需要了解数据中哪些测量值表示问题,并编写代码来有意识地检查它们。

这种方法对许多问题都有效。例如,我们知道水在海平面上沸腾的温度是 100°C,因此可以轻松编写一个程序,根据当前温度和海拔高度来预测水是否正在沸腾。但在许多情况下,很难知道哪些因素的确切组合预测了给定状态。继续以我们的工厂机器示例为例,可能有各种不同的生产速率、温度和振动水平的组合可能表明问题,但从数据中看不出来。

为了创建一个机器学习程序,程序员将数据输入到一种特殊类型的算法中,让算法发现规则。这意味着作为程序员,我们可以创建基于复杂数据的预测程序,而不必完全理解所有复杂性。机器学习算法基于我们提供的数据构建系统的模型,通过我们称之为训练的过程。模型是一种计算机程序。我们通过这个模型运行数据来进行预测,这个过程称为推理

机器学习有许多不同的方法。其中最流行的之一是深度学习,它基于人类大脑可能如何工作的简化想法。在深度学习中,一组模拟神经元(由数字数组表示)被训练来模拟各种输入和输出之间的关系。不同的架构或模拟神经元的排列对不同的任务很有用。例如,一些架构擅长从图像数据中提取含义,而其他架构最适合预测序列中的下一个值。

本书中的示例侧重于深度学习,因为它是解决适合微控制器的问题类型的灵活且强大的工具。也许令人惊讶的是,深度学习甚至可以在内存和处理能力有限的设备上运行。事实上,在本书的过程中,您将学习如何创建一些非常惊人的深度学习模型,但这些模型仍然符合微型设备的限制。

下一节解释了创建和使用深度学习模型的基本工作流程。

深度学习工作流程

在前一节中,我们概述了使用深度学习来预测工厂机器何时可能会发生故障的场景。在本节中,我们介绍了使这一情况发生所需的工作。

这个过程将涉及以下任务:

  1. 确定目标

  2. 收集数据集

  3. 设计模型架构

  4. 训练模型

  5. 转换模型

  6. 运行推理

  7. 评估和故障排除

让我们逐一走过它们。

确定目标

当您设计任何类型的算法时,重要的是首先明确您希望它做什么。机器学习也不例外。您需要决定您想要预测什么,以便确定要收集哪些数据以及使用哪种模型架构。

在我们的例子中,我们想要预测我们的工厂机器是否即将发生故障。我们可以将这表示为一个分类问题。分类是一个机器学习任务,它接受一组输入数据,并返回这些数据符合一组已知类别的概率。在我们的例子中,我们可能有两个类别:“正常”,表示我们的机器正常运行,没有问题,“异常”,表示我们的机器显示出可能很快会发生故障的迹象。

这意味着我们的目标是创建一个将我们的输入数据分类为“正常”或“异常”的模型。

收集数据集

我们的工厂可能有大量可用数据,从机器的运行温度到某一天食堂提供的食物类型。鉴于我们刚刚建立的目标,我们可以开始确定我们需要的数据。

选择数据

深度学习模型可以学会忽略嘈杂或无关的数据。也就是说,最好只使用与解决问题相关的信息来训练模型。由于今天的食堂食物不太可能影响我们机器的运行,我们可能可以将其从数据集中排除。否则,模型将需要学会否定那些无关的输入,并且可能容易学习到虚假的关联——也许我们的机器总是在提供比萨的日子出故障。

在决定是否包含数据时,您应该始终尝试将领域专业知识与实验相结合。您还可以使用统计技术来尝试识别哪些数据是重要的。如果您仍然不确定是否包含某个数据源,您可以始终训练两个模型,看哪个效果最好!

假设我们已经确定了最有前途的数据为生产速率温度振动。我们的下一步是收集一些数据,以便我们可以训练一个模型。

提示

选择的数据在您想要进行预测时也是可用的非常重要。例如,由于我们决定用温度读数训练我们的模型,当我们进行推理时,我们将需要提供来自完全相同物理位置的温度读数。这是因为模型学习了如何理解其输入如何预测其输出。如果我们最初在机器内部的温度数据上训练模型,那么在当前室温上运行模型可能不起作用。

收集数据

要训练有效的模型需要多少数据是很难确定的。这取决于许多因素,例如变量之间的关系复杂性、噪音量以及类别之间的区分程度。然而,有一个经验法则始终成立:数据越多,越好!

你应该努力收集代表系统中可能发生的所有条件和事件的数据。如果我们的机器可能以几种不同的方式出现故障,我们应该确保捕获每种类型故障周围的数据。如果一个变量随着时间自然变化,收集代表整个范围的数据是很重要的。例如,如果机器在温暖的日子里温度升高,你应该确保包括冬天和夏天的数据。这种多样性将帮助你的模型代表每种可能的情况,而不仅仅是一些特定的情况。

我们收集关于工厂的数据可能会被记录为一组时间序列,意味着定期收集的一系列读数。例如,我们可能每分钟记录一次温度,每小时记录一次生产速率,每秒记录一次振动水平。在收集数据后,我们需要将这些时间序列转换为适合我们模型的形式。

标记数据

除了收集数据,我们还需要确定哪些数据代表“正常”和“异常”操作。我们将在训练过程中提供这些信息,以便我们的模型学习如何对输入进行分类。将数据与类别相关联的过程称为标记,而“正常”和“异常”类别是我们的标签

注意

这种训练方式,即在训练期间指导算法数据的含义,被称为监督学习。生成的分类模型将能够处理传入的数据并预测其可能属于哪个类别。

为了标记我们收集到的时间序列数据,我们需要记录机器工作和故障的时间段。我们可能会假设机器故障前的时间段通常代表异常操作。然而,由于我们不能从数据的表面看出异常操作,正确地获取这些信息可能需要一些实验!

在我们决定如何标记数据之后,我们可以生成一个包含标签的时间序列,并将其添加到我们的数据集中。

我们的最终数据集

表 3-1 列出了我们在工作流程中此刻已经收集的数据源。

表 3-1. 数据源

数据源间隔样本读数
生产速率每 2 分钟一次100 个单位
温度每分钟一次30°C
振动(典型值的百分比)每 10 秒一次23%
标签(“正常”或“异常”)每 10 秒一次正常

表格显示了每个数据源的时间间隔。例如,温度每分钟记录一次。我们还生成了一个包含数据标签的时间序列。我们的标签间隔是每 10 秒 1 次,与其他时间序列的最小间隔相同。这意味着我们可以轻松确定数据中每个数据点的标签。

现在我们已经收集了数据,是时候用它来设计和训练模型了。

设计模型架构

有许多类型的深度学习模型架构,旨在解决各种问题。在训练模型时,您可以选择设计自己的架构或基于研究人员开发的现有架构。对于许多常见问题,您可以在网上找到免费的预训练模型。

在本书的过程中,我们将向您介绍几种不同的模型架构,但除了这里介绍的内容外,还有大量可能性。设计模型既是一门艺术也是一门科学,模型架构是一个重要的研究领域。每天都会有新的架构被发明。

在决定架构时,您需要考虑您试图解决的问题类型、您可以访问的数据类型以及在将数据馈送到模型之前可以对数据进行的转换方式(我们将很快讨论数据转换)。事实上,由于最有效的架构取决于您正在处理的数据类型,您的数据和模型架构是紧密相连的。尽管我们在这里分开标题介绍它们,但它们总是会被一起考虑。

您还需要考虑将在其上运行模型的设备的约束,因为微控制器通常具有有限的内存和较慢的处理器,较大的模型需要更多内存并需要更长时间运行 - 模型的大小取决于它包含的神经元数量以及这些神经元的连接方式。此外,一些设备配备了硬件加速功能,可以加快某些类型的模型架构的执行速度,因此您可能希望根据您考虑的设备的优势来定制您的模型。

在我们的情况下,我们可能首先通过几层神经元训练一个简单模型,然后通过迭代过程优化架构,直到获得有用的结果。您将在本书的后面看到如何做到这一点。

深度学习模型接受输入并生成张量形式的输出。对于本书的目的,¹ 张量本质上是一个可以包含数字或其他张量的列表;您可以将其视为类似于数组。我们的假设简单模型将以张量作为输入。以下小节描述了我们如何将数据转换为这种形式。

从数据生成特征

我们已经确定我们的模型将接受某种张量作为输入。但正如我们之前讨论的,我们的数据以时间序列的形式呈现。我们如何将时间序列数据转换为可以传递到模型中的张量呢?

我们现在的任务是决定如何从我们的数据中生成特征。在机器学习中,术语特征指的是模型训练的特定类型信息。不同类型的模型训练在不同类型的特征上。例如,一个模型可能接受一个单一标量值作为其唯一输入特征。

但是输入可能比这更复杂:一个设计用于处理图像的模型可能接受一个多维张量的图像数据作为输入,而一个设计用于基于多个特征进行预测的模型可能接受一个包含多个标量值的向量,每个特征对应一个值。

回想一下,我们决定我们的模型应该使用生产速率、温度和振动来进行预测。以它们的原始形式,作为具有不同间隔的时间序列,这些数据将不适合传递给模型。下一节将解释原因。

窗口化

在下图中,我们的时间序列中的每个数据都用一个星号表示。当前标签包含在数据中,因为标签是训练所必需的。我们的目标是训练一个模型,可以根据当前条件在任何给定时刻预测机器是正常运行还是异常运行:

Production:    *                       *            (every 2 minutes)
Temperature:   *           *           *            (every minute)
Vibration:     * * * * * * * * * * * * * * * * *    (every 10 seconds)
Label:         * * * * * * * * * * * * * * * * *    (every 10 seconds)

然而,由于我们的时间序列具有不同的间隔(比如每分钟一次,或者每 10 秒一次),如果我们只传入给定时刻可用的数据,可能不包括我们可用的所有数据类型。例如,在下图中突出显示的时刻,只有振动数据可用。这意味着我们的模型在尝试进行预测时只有振动信息:

                                              ┌─┐
Production:    *                       *      │ │
Temperature:   *           *           *      │ │
Vibration:     * * * * * * * * * * * * * * * *│*│
Label:         * * * * * * * * * * * * * * * *│*│
                                              └─┘

解决这个问题的一个方法可能是选择一个时间窗口,并将该窗口内的所有数据合并为一组数值。例如,我们可以决定使用一个一分钟的时间窗口,并查看其中包含的所有数值:

                                    ┌───────────┐
Production:    *                    │  *        │
Temperature:   *           *        │  *        │
Vibration:     * * * * * * * * * * *│* * * * * *│
Label:         * * * * * * * * * * *│* * * * * *│
                                    └───────────┘

如果我们对每个时间序列的窗口中的所有值求平均,并对当前窗口中缺少数据点的任何值取最近的值,我们最终得到一组单一值。我们可以根据窗口中是否存在任何“异常”标签来决定如何标记这个快照。如果窗口中有任何“异常”存在,窗口应该被标记为“异常”。如果没有,应该被标记为“正常”:

                                    ┌───────────┐
Production:    *                    │  *        │  Average: 102
Temperature:   *           *        │  *        │  Average: 34°C
Vibration:     * * * * * * * * * * *│* * * * * *│  Average: 18%
Label:         * * * * * * * * * * *│* * * * * *│  Label:   "normal"
                                    └───────────┘

这三个非标签数值是我们的特征!我们可以将它们作为一个向量传递给我们的模型,每个时间序列有一个元素:

[102 34 .18]

在训练过程中,我们可以为每 10 秒的数据计算一个新的窗口,并将其传递给我们的模型,使用标签来通知训练算法我们期望的输出。在推断过程中,每当我们想要使用模型来预测异常行为时,我们只需查看我们的数据,计算最近的窗口,将其通过模型运行,并接收一个预测。

这是一个简单的方法,实际上可能并不总是有效,但这是一个很好的起点。您很快会发现,机器学习就是试错的过程!

在我们继续训练之前,让我们再谈一下关于输入数值的最后一点。

归一化

通常,您向神经网络提供的数据将以填充有浮点值或浮点数的张量形式呈现。浮点数是一种用于表示具有小数点的数字的数据类型。为了让训练算法有效地工作,这些浮点数值需要在大小上相似。事实上,如果所有数值都表示为 0 到 1 范围内的数字,那将是理想的。

让我们再次看一下上一节中的输入张量:

[102 34 .18]

这些数值在非常不同的尺度上:温度超过 100,而振动表示为 1 的分数。为了将这些值传递给我们的网络,我们需要对它们进行归一化,使它们都在一个类似的范围内。

一种方法是计算数据集中每个特征的平均值,并从值中减去。这样做的效果是将数字压缩到接近零。这里有一个例子:

Temperature series:
[108 104 102 103 102]

Mean:
103.8

Normalized values, calculated by subtracting 103.8 from each temperature:
[ 4.2 0.2 -1.8 -0.8 -1.8 ]

您经常会遇到归一化的情况之一是当图像被输入神经网络时,以不同的方式实现。计算机通常将图像存储为 8 位整数的矩阵,其值范围从 0 到 255。为了使这些值归一化,使它们都在 0 到 1 之间,每个 8 位值都乘以1/255。这里有一个示例,其中包含一个 3×3 像素的灰度图像,其中每个像素的值表示其亮度:

Original 8-bit values:
[[255 175 30]
 [0   45  24]
 [130 192 87]]

Normalized values:
[[1\.         0.68627451 0.11764706]
 [0\.         0.17647059 0.09411765]
 [0.50980392 0.75294118 0.34117647]]

用机器学习思考

到目前为止,我们已经学会了如何开始用机器学习解决问题。在我们的工厂场景中,我们已经决定了一个合适的目标,收集和标记了适当的数据,设计了要传递到模型中的特征,并选择了一个模型架构。无论我们试图解决什么问题,我们都会使用相同的方法。重要的是要注意,这是一个迭代过程,我们经常在 ML 工作流程的各个阶段之间来回,直到我们找到一个有效的模型,或者决定任务太困难。

例如,想象一下我们正在构建一个预测天气的模型。我们需要决定我们的目标(例如,预测明天是否会下雨),收集和标记数据集(例如过去几年的天气报告),设计我们将传递给模型的特征(也许是过去两天的平均条件),并选择适合这种数据类型和我们要运行的设备的模型架构。我们会想出一些初始想法,测试它们,并调整我们的方法,直到获得良好的结果。

我们工作流程中的下一步是训练,我们将在以下部分中探讨。

训练模型

训练是模型学习为给定的输入集合产生正确输出的过程。它涉及将训练数据输入模型,并对其进行小的调整,直到它能够做出最准确的预测。

正如我们之前讨论的,模型是由排列成层的数字数组表示的模拟神经元网络。这些数字被称为权重偏置,或者统称为网络的参数

当数据被输入网络时,它会通过每一层中的权重和偏置进行连续的数学运算进行转换。模型的输出是通过这些操作运行输入的结果。图 3-1 显示了一个具有两层的简单网络。

模型的权重从随机值开始,偏置通常从值 0 开始。在训练过程中,将数据的批次输入模型,并将模型的输出与期望输出(在我们的情况下是正确的标签“正常”或“异常”)进行比较。一种称为反向传播的算法逐渐调整权重和偏置,以使随着时间的推移,模型的输出越来越接近期望值。训练以周期(意味着迭代)来衡量,直到我们决定停止为止。

一个简单的深度学习网络

图 3-1。一个具有两层的简单深度学习网络

通常情况下,当一个模型的性能停止改善时,我们会停止训练。当它开始做出准确的预测时,就说它已经收敛。为了确定一个模型是否已经收敛,我们可以分析其在训练过程中的性能图表。两个常见的性能指标是损失准确性。损失指标给出了一个数值估计,表明模型离产生预期答案有多远,而准确性指标告诉我们它选择正确预测的百分比。一个完美的模型将具有 0.0 的损失和 100%的准确性,但真实模型很少是完美的。

图 3-2 显示了深度学习网络在训练过程中的损失和准确性。您可以看到随着训练的进行,准确性增加,损失减少,直到达到一个模型不再改善的点。

为了尝试改善模型的性能,我们可以改变模型的架构,调整用于设置模型和调节训练过程的各种值。这些值被统称为超参数,它们包括诸如要运行的训练周期数和每个层中的神经元数等变量。每次我们进行更改时,我们可以重新训练模型,查看指标,并决定是否进一步优化。希望,时间和迭代将产生一个具有可接受准确性的模型!

显示训练过程中模型收敛的图表

图 3-2。显示训练过程中模型收敛的图表
注意

重要的是要记住,并没有保证你能够达到足够好的准确性来解决你正在尝试解决的问题。数据集中并不总是包含足够的信息来进行准确的预测,有些问题甚至无法解决,即使使用最先进的深度学习技术也不行。也就是说,即使模型不是 100%准确,它也可能是有用的。在我们的工厂示例中,即使只能部分时间预测异常操作也可能会大有帮助。

欠拟合和过拟合

模型无法收敛的两个最常见原因是欠拟合过拟合

神经网络学习拟合其在数据中识别的模式。如果一个模型被正确拟合,它将为给定的输入产生正确的输出。当一个模型欠拟合时,它还没有能够学习到足够强的这些模式的表示形式,以便能够做出良好的预测。这可能由于各种原因导致,最常见的是架构太小,无法捕捉应该建模的系统的复杂性,或者没有足够的数据进行训练。

当一个模型过拟合时,它已经对其训练数据学习得太好了。模型能够准确预测其训练数据的细微之处,但它无法将其学习推广到以前没有见过的数据。通常情况下,这是因为模型已经完全记住了训练数据,或者它已经学会依赖于训练数据中存在但现实世界中不存在的一种捷径。

例如,想象一下,你正在训练一个模型来将照片分类为包含狗或猫。如果你的训练数据中所有的狗照片都是在室外拍摄的,而所有的猫照片都是在室内拍摄的,你的模型可能会学会作弊,并利用每张照片中天空的存在来预测是哪种动物。这意味着如果未来的狗自拍照片恰好是在室内拍摄的话,它可能会错误分类。

有许多方法来对抗过拟合。一种可能性是减小模型的大小,使其没有足够的容量来学习其训练集的精确表示。一组称为正则化的技术可以在训练过程中应用,以减少过拟合的程度。为了充分利用有限的数据,可以使用一种称为数据增强的技术,通过切片和切块现有数据来生成新的人工数据点。但是,打败过拟合的最佳方法,如果可能的话,是获得一个更大更多样化的数据集。更多的数据总是有帮助的!

训练、验证和测试

要评估模型的性能,我们可以看看它在训练数据上的表现如何。然而,这只告诉我们故事的一部分。在训练过程中,模型学会尽可能紧密地拟合其训练数据。正如我们之前看到的,在某些情况下,模型将开始过拟合训练数据,这意味着它在训练数据上表现良好,但在现实生活中却不行。

要了解何时发生这种情况,我们需要使用新数据验证模型,这些数据在训练中没有使用。将数据集分成三部分——训练验证测试是常见的。典型的分割是 60%的训练数据,20%的验证数据和 20%的测试数据。这种分割必须这样做,以便每个部分包含相同的信息分布,并以保持数据结构的方式进行。例如,由于我们的数据是时间序列,我们可以将其潜在地分成三个连续的时间段。如果我们的数据不是时间序列,我们可以随机抽样数据点。

在训练过程中,训练数据集用于训练模型。定期,来自验证数据集的数据被馈送到模型中,并计算损失。因为模型以前没有见过这些数据,所以它的损失分数是模型表现的更可靠指标。通过比较训练和验证损失(以及准确性,或其他可用的指标),您可以看到模型是否过拟合。

图 3-3 显示了一个过拟合的模型。您可以看到随着训练损失的降低,验证损失却上升了。这意味着模型在预测训练数据方面变得更好,但失去了对新数据的泛化能力。

显示模型在训练过程中过拟合的图表

图 3-3. 显示模型在训练过程中过拟合的图表

当我们调整我们的模型和训练过程以提高性能并避免过拟合时,我们希望看到我们的验证指标得到改善。

然而,这个过程有一个不幸的副作用。通过优化以改善验证指标,我们可能只是在推动模型朝着过拟合训练数据验证数据的方向!我们所做的每一个调整都会使模型稍微更好地适应验证数据,最终,我们可能会遇到与之前相同的过拟合问题。

为了验证这种情况没有发生,我们在训练模型的最后一步是在我们的测试数据上运行它,并确认它的表现与验证期间一样好。如果没有,我们已经优化了我们的模型以过拟合我们的训练和验证数据。在这种情况下,我们可能需要回到起点,提出一个新的模型架构,因为如果我们继续调整以提高在测试数据上的表现,我们也会过拟合到那里。

当我们有一个在训练、验证和测试数据上表现良好的模型后,这个过程的训练部分就结束了。接下来,我们准备好在设备上运行我们的模型!

转换模型

在整本书中,我们使用 TensorFlow 来构建和训练模型。一个 TensorFlow 模型本质上是一组指令,告诉一个解释器如何转换数据以产生输出。当我们想要使用我们的模型时,我们只需将其加载到内存中,并使用 TensorFlow 解释器执行它。

然而,TensorFlow 的解释器是设计用于在强大的台式计算机和服务器上运行模型的。由于我们将在微型微控制器上运行我们的模型,我们需要一个专为我们的用例设计的不同解释器。幸运的是,TensorFlow 提供了一个解释器和相关工具,用于在小型、低功耗设备上运行模型。这套工具称为 TensorFlow Lite。

在 TensorFlow Lite 可以运行模型之前,首先必须将其转换为 TensorFlow Lite 格式,然后保存到磁盘上作为文件。我们使用一个名为TensorFlow Lite Converter的工具来完成这个过程。转换器还可以应用特殊优化,旨在减小模型的大小并帮助其运行更快,通常不会牺牲性能。

在第十三章中,我们深入探讨了 TensorFlow Lite 的细节以及它如何帮助我们在微小设备上运行模型。目前,你只需要知道你需要转换你的模型,并且转换过程快速简单。

运行推断

模型转换后,就可以部署了!我们现在将使用 TensorFlow Lite for Microcontrollers C++ 库来加载模型并进行预测。

由于这是我们的模型与应用代码相遇的部分,我们需要编写一些代码,从传感器获取原始输入数据并将其转换为模型训练的相同形式。然后,我们将这些转换后的数据传递给我们的模型并运行推断。

这将导致包含预测的输出数据。在我们的分类器模型的情况下,输出将是每个类别“正常”和“异常”的得分。对于分类数据的模型,通常所有类别的得分将总和为 1,得分最高的类别将是预测。得分之间的差异越大,对预测的置信度就越高。表 3-2 列出了一些示例输出。

表 3-2. 示例输出

正常得分异常得分解释
0.10.9在异常状态下有高置信度
0.90.1在正常状态下有高置信度
0.70.3对正常状态有轻微置信度
0.490.51结果不确定,因为两种状态都没有明显领先

在我们的工厂机器示例中,每个单独的推断仅考虑数据的一个快照——它告诉我们在过去 10 秒内出现异常状态的概率,基于各种传感器读数。由于现实世界的数据通常混乱,机器学习模型并不完美,因此可能会出现临时故障导致错误分类的情况。例如,由于临时传感器故障,我们可能会看到温度值的突然上升。这种瞬态、不可靠的输入可能导致输出分类短暂地不符合现实。

为了防止这些瞬时故障导致问题,我们可能会在一段时间内对模型的所有输出取平均值。例如,我们可以每 10 秒在当前数据窗口上运行我们的模型,并取最后 6 个输出的平均值,以给出每个类别的平滑得分。这意味着瞬时问题被忽略,我们只对一致的行为采取行动。我们使用这种技术来帮助唤醒词检测在第七章。

在为每个类别得分后,由我们的应用代码决定如何处理。也许如果连续检测到异常状态一分钟,我们的代码将发送信号关闭机器并通知维护团队。

评估和故障排除

在我们部署了模型并在设备上运行后,我们将开始看到其真实世界的性能是否达到我们的期望。即使我们已经证明我们的模型在测试数据上做出了准确的预测,但在实际问题上的表现可能会有所不同。

出现这种情况可能有很多原因。例如,训练中使用的数据可能并不完全代表实际操作中可用的数据。也许由于当地气候,我们机器的温度通常比我们收集数据集的那台机器要凉爽。这可能会影响我们模型的预测,使其不再像预期的那样准确。

另一个可能性是我们的模型可能已经过度拟合我们的数据集,而我们没有意识到。在“训练模型”中,我们学到了当数据集恰好包含模型可以学习识别的额外信号时,这种情况可能会发生。

如果我们的模型在生产中不起作用,我们需要进行一些故障排除。首先,我们排除可能影响到达我们模型的数据的任何硬件问题(如故障传感器或意外噪音)。其次,我们从部署模型的设备中捕获一些数据,并将其与我们的原始数据集进行比较,以确保它在同一范围内。如果不是,也许环境条件或传感器特性存在差异,而我们没有预料到。如果数据检查通过,可能过拟合是问题所在。

在排除硬件问题后,解决过拟合问题的最佳方法通常是使用更多数据进行训练。我们可以从部署的硬件中捕获额外的数据,将其与原始数据集结合起来,然后重新训练我们的模型。在这个过程中,我们可以应用正则化和数据增强技术,以帮助充分利用我们拥有的数据。

要达到良好的实际性能,有时可能需要对模型、硬件和相关软件进行一些迭代。如果遇到问题,要像处理其他技术问题一样对待。采用科学方法进行故障排除,排除可能的因素,并分析数据以找出问题所在。

总结

现在您已经熟悉了机器学习从业者使用的基本工作流程,我们准备在 TinyML 冒险中迈出下一步。

在第四章中,我们将构建我们的第一个模型并将其部署到一些微型硬件上!

这个对于“张量”一词的定义与数学和物理学对该词的定义不同,但在数据科学中已成为常态。

第四章:TinyML 的“Hello World”:构建和训练模型

在第三章中,我们学习了机器学习的基本概念以及机器学习项目遵循的一般工作流程。在本章和下一章中,我们将开始将我们的知识付诸实践。我们将从头开始构建和训练一个模型,然后将其集成到一个简单的微控制器程序中。

在这个过程中,您将通过一些强大的开发者工具亲自动手,这些工具每天都被尖端机器学习从业者使用。您还将学习如何将机器学习模型集成到 C++程序中,并将其部署到微控制器以控制电路中的电流。这可能是您第一次尝试混合硬件和机器学习,应该很有趣!

您可以在 Mac、Linux 或 Windows 机器上测试我们在这些章节中编写的代码,但要获得完整的体验,您需要其中一个嵌入式设备,如“需要哪些硬件?”中提到的。

创建我们的机器学习模型,我们将使用 Python、TensorFlow 和 Google 的 Colaboratory,这是一个基于云的交互式笔记本,用于尝试 Python 代码。这些是真实世界中机器学习工程师最重要的工具之一,而且它们都是免费使用的。

注意

想知道本章标题的含义吗?在编程中,引入新技术通常会附带演示如何做一些非常简单的事情的示例代码。通常,这个简单的任务是使程序输出“Hello, world.”这些词。在机器学习中没有明确的等价物,但我们使用术语“hello world”来指代一个简单、易于阅读的端到端 TinyML 应用程序的示例。

在本章的过程中,我们将执行以下操作:

  1. 获取一个简单的数据集。

  2. 训练一个深度学习模型。

  3. 评估模型的性能。

  4. 将模型转换为在设备上运行。

  5. 编写代码执行设备推断。

  6. 将代码构建成二进制文件。

  7. 将二进制部署到微控制器。

我们将使用的所有代码都可以在TensorFlow 的 GitHub 存储库中找到。

我们建议您逐步阅读本章的每个部分,然后尝试运行代码。沿途会有如何操作的说明。但在我们开始之前,让我们讨论一下我们要构建的内容。

我们正在构建什么

在第三章中,我们讨论了深度学习网络如何学习模拟其训练数据中的模式,以便进行预测。现在我们将训练一个网络来模拟一些非常简单的数据。您可能听说过正弦函数。它在三角学中用于帮助描述直角三角形的性质。我们将使用的数据是正弦波,这是通过绘制随时间变化的正弦函数的结果得到的图形(请参见图 4-1)。

我们的目标是训练一个模型,可以接受一个值x,并预测其正弦值y。在实际应用中,如果您需要x的正弦值,您可以直接计算它。然而,通过训练一个模型来近似结果,我们可以演示机器学习的基础知识。

我们项目的第二部分将是在硬件设备上运行这个模型。从视觉上看,正弦波是一个愉悦的曲线,从-1 平稳地运行到 1,然后返回。这使得它非常适合控制一个视觉上令人愉悦的灯光秀!我们将使用我们模型的输出来控制一些闪烁的 LED 或图形动画的时间,具体取决于设备的功能。

随时间变化的正弦函数图

图 4-1. 正弦波

在线,您可以看到这段代码闪烁 SparkFun Edge 的 LED 的动画 GIF。图 4-2 是来自此动画的静止图像,显示了设备的几个 LED 灯亮起。这可能不是机器学习的特别有用的应用,但在“hello world”示例的精神中,它简单,有趣,并将有助于演示您需要了解的基本原则。

在我们的基本代码运行后,我们将部署到三种不同的设备:SparkFun Edge,Arduino Nano 33 BLE Sense 和 ST Microelectronics STM32F746G Discovery 套件。

注意

由于 TensorFlow 是一个不断发展的积极开发的开源项目,您可能会注意到此处打印的代码与在线托管的代码之间存在一些细微差异。不用担心,即使有几行代码发生变化,基本原则仍然保持不变。

显示 SparkFun Edge 带有两个 LED 灯亮起的视频静止图像

图 4-2。在 SparkFun Edge 上运行的代码

我们的机器学习工具链

为了构建这个项目的机器学习部分,我们正在使用真实世界机器学习从业者使用的相同工具。本节向您介绍这些工具。

Python 和 Jupyter 笔记本

Python 是机器学习科学家和工程师最喜欢的编程语言。它易于学习,适用于许多不同的应用程序,并且有大量用于涉及数据和数学的有用任务的库。绝大多数深度学习研究都是使用 Python 进行的,研究人员经常发布他们创建的模型的 Python 源代码。

Python 与一种称为Jupyter 笔记本结合使用时特别好。这是一种特殊的文档格式,允许您混合编写、图形和代码,可以在点击按钮时运行。Jupyter 笔记本被广泛用作描述、解释和探索机器学习代码和问题的一种方式。

我们将在 Jupyter 笔记本中创建我们的模型,这使我们能够在开发过程中对我们的数据进行可视化。这包括显示显示我们模型准确性和收敛性的图形。

如果您有一些编程经验,Python 易于阅读和学习。您应该能够在没有任何困难的情况下跟随本教程。

谷歌 Colaboratory

为了运行我们的笔记本,我们将使用一个名为Colaboratory的工具,简称为Colab。Colab 由谷歌制作,它提供了一个在线环境来运行 Jupyter 笔记本。它作为一个免费工具提供,以鼓励机器学习中的研究和开发。

传统上,您需要在自己的计算机上创建一个笔记本。这需要安装许多依赖项,如 Python 库,这可能会让人头疼。与其他人分享结果笔记本也很困难,因为他们可能有不同版本的依赖项,这意味着笔记本可能无法按预期运行。此外,机器学习可能需要大量计算,因此在开发计算机上训练模型可能会很慢。

Colab 允许您在谷歌强大的硬件上免费运行笔记本。您可以从任何网络浏览器编辑和查看您的笔记本,并与其他人分享,他们在运行时保证获得相同的结果。您甚至可以配置 Colab 在专门加速的硬件上运行您的代码,这样可以比普通计算机更快地进行训练。

TensorFlow 和 Keras

TensorFlow是一套用于构建、训练、评估和部署机器学习模型的工具。最初由谷歌开发,TensorFlow 现在是一个由全球数千名贡献者构建和维护的开源项目。它是最受欢迎和广泛使用的机器学习框架。大多数开发人员通过其 Python 库与 TensorFlow 进行交互。

TensorFlow 可以做很多不同的事情。在本章中,我们将使用Keras,这是 TensorFlow 的高级 API,使构建和训练深度学习网络变得容易。我们还将使用TensorFlow Lite,这是一组用于在移动和嵌入式设备上部署 TensorFlow 模型的工具,以在设备上运行我们的模型。

第十三章将更详细地介绍 TensorFlow。现在,只需知道它是一个非常强大和行业标准的工具,将在您从初学者到深度学习专家的过程中继续满足您的需求。

构建我们的模型

现在我们将逐步介绍构建、训练和转换模型的过程。我们在本章中包含了所有的代码,但您也可以在 Colab 中跟着进行并运行代码。

首先,加载笔记本。页面加载后,在顶部,单击“在 Google Colab 中运行”按钮,如图 4-3 所示。这将把笔记本从 GitHub 复制到 Colab,允许您运行它并进行编辑。

“在 Google Colab 中运行”按钮

图 4-3. “在 Google Colab 中运行”按钮

默认情况下,除了代码外,笔记本还包含您在运行代码时应该看到的输出样本。由于我们将在本章中运行代码,让我们清除这些输出,使笔记本处于原始状态。要做到这一点,在 Colab 的菜单中,单击“编辑”,然后选择“清除所有输出”,如图 4-4 所示。

“清除所有输出”选项

图 4-4. “清除所有输出”选项

干得好。我们的笔记本现在已经准备好了!

提示

如果您已经熟悉机器学习、TensorFlow 和 Keras,您可能想直接跳到我们将模型转换为 TensorFlow Lite 使用的部分。在书中,跳到“将模型转换为 TensorFlow Lite”。在 Colab 中,滚动到“转换为 TensorFlow Lite”标题下。

导入依赖项

我们的第一个任务是导入我们需要的依赖项。在 Jupyter 笔记本中,代码和文本被安排在单元格中。有代码单元格,其中包含可执行的 Python 代码,以及文本单元格,其中包含格式化的文本。

我们的第一个代码单元格位于“导入依赖项”下面。它设置了我们需要训练和转换模型的所有库。以下是代码:

# TensorFlow is an open source machine learning library
!pip install tensorflow==2.0
import tensorflow as tf
# NumPy is a math library
import numpy as np
# Matplotlib is a graphing library
import matplotlib.pyplot as plt
# math is Python's math library
import math

在 Python 中,import语句加载一个库,以便我们的代码可以使用它。您可以从代码和注释中看到,这个单元格执行以下操作:

  • 使用pip安装 TensorFlow 2.0 库,pip是 Python 的软件包管理器

  • 导入 TensorFlow、NumPy、Matplotlib 和 Python 的math

当我们导入一个库时,我们可以给它一个别名,以便以后容易引用。例如,在前面的代码中,我们使用import numpy as np导入 NumPy,并给它别名np。当我们在代码中使用它时,可以将其称为np

代码单元格中的代码可以通过单击出现在左上角的按钮来运行,当单元格被选中时会出现该按钮。在“导入依赖项”部分,单击第一个代码单元格的任何位置,使其被选中。图 4-5 显示了选定单元格的外观。

“导入依赖项”单元格处于选定状态

图 4-5. “导入依赖项”单元格处于选定状态

要运行代码,请单击左上角出现的按钮。当代码正在运行时,按钮将以圆圈的形式显示动画,如图 4-6 所示。

依赖项将开始安装,并会看到一些输出。最终您应该看到以下行,表示库已成功安装:

Successfully installed tensorboard-2.0.0 tensorflow-2.0.0 tensorflow-estimator-2.0.0

“导入依赖项”单元格处于运行状态

图 4-6. “导入依赖项”单元格处于运行状态

在 Colab 中运行一个单元格后,当它不再被选中时,您会看到左上角显示一个1,如图 4-7 所示。这个数字是一个计数器,每次运行单元格时都会递增。

左上角的单元格运行计数器

图 4-7. 左上角的单元格运行计数器

您可以使用这个来了解哪些单元格已经运行过,以及运行了多少次。

生成数据

深度学习网络学习对底层数据的模式进行建模。正如我们之前提到的,我们将训练一个网络来模拟由正弦函数生成的数据。这将导致一个模型,可以接受一个值x,并预测它的正弦值y

在继续之前,我们需要一些数据。在现实世界的情况下,我们可能会从传感器和生产日志中收集数据。然而,在这个例子中,我们使用一些简单的代码来生成数据集。

接下来的单元格就是这样的。我们的计划是生成 1,000 个代表正弦波上随机点的值。让我们看一下图 4-8 来提醒自己正弦波是什么样子的。

波的每个完整周期称为它的周期。从图中,我们可以看到每隔大约六个单位在x轴上完成一个完整周期。事实上,正弦波的周期是 2 × π,或 2π。

为了训练完整的正弦波数据,我们的代码将生成从 0 到 2π的随机x值。然后将计算每个这些值的正弦值。

随时间变化的正弦函数图表

图 4-8. 一个正弦波

这是这个单元格的完整代码,它使用 NumPy(我们之前导入的np)生成随机数并计算它们的正弦值:

# We'll generate this many sample datapoints
SAMPLES = 1000

# Set a "seed" value, so we get the same random numbers each time we run this
# notebook. Any number can be used here.
SEED = 1337
np.random.seed(SEED)
tf.random.set_seed(SEED)

# Generate a uniformly distributed set of random numbers in the range from
# 0 to 2π, which covers a complete sine wave oscillation
x_values = np.random.uniform(low=0, high=2*math.pi, size=SAMPLES)

# Shuffle the values to guarantee they're not in order
np.random.shuffle(x_values)

# Calculate the corresponding sine values
y_values = np.sin(x_values)

# Plot our data. The 'b.' argument tells the library to print blue dots.
plt.plot(x_values, y_values, 'b.')
plt.show()

除了我们之前讨论的内容,这段代码中还有一些值得指出的地方。首先,您会看到我们使用np.random.uniform()来生成我们的x值。这个方法返回指定范围内的随机数数组。NumPy 包含许多有用的方法,可以操作整个值数组,这在处理数据时非常方便。

其次,在生成数据后,我们对数据进行了洗牌。这很重要,因为深度学习中使用的训练过程取决于以真正随机的顺序提供数据。如果数据是有序的,那么生成的模型将不够准确。

接下来,请注意我们使用 NumPy 的sin()方法来计算正弦值。NumPy 可以一次为所有x值执行此操作,返回一个数组。NumPy 太棒了!

最后,您会看到一些神秘的代码调用plt,这是我们对 Matplotlib 的别名:

# Plot our data. The 'b.' argument tells the library to print blue dots.
plt.plot(x_values, y_values, 'b.')
plt.show()

这段代码是做什么的?它绘制了我们数据的图表。Jupyter 笔记本的一个最好的地方是它们能够显示代码运行输出的图形。Matplotlib 是一个从数据创建图表的优秀工具。由于可视化数据是机器学习工作流程的重要部分,这将在我们训练模型时非常有帮助。

要生成数据并将其呈现为图表,请运行单元格中的代码。代码单元格运行完成后,您应该会看到一个漂亮的图表出现在下面,就像图 4-9 中显示的那样。

我们生成数据的图表

图 4-9. 我们生成数据的图表

这就是我们的数据!这是沿着一个漂亮、平滑的正弦曲线的随机点的选择。我们可以使用这个来训练我们的模型。然而,这样做太容易了。深度学习网络的一个令人兴奋的地方是它们能够从噪音中提取模式。这使它们能够在训练混乱的真实世界数据时进行预测。为了展示这一点,让我们向我们的数据点添加一些随机噪音并绘制另一个图表:

# Add a small random number to each y value
y_values += 0.1 * np.random.randn(*y_values.shape)

# Plot our data
plt.plot(x_values, y_values, 'b.')
plt.show()

运行这个单元格,看看结果,如图 4-10 所示。

更好了!我们的点现在已经随机化,因此它们代表了围绕正弦波的分布,而不是平滑的完美曲线。这更加反映了现实世界的情况,其中数据通常相当混乱。

我们的数据添加了噪声的图

图 4-10。我们的数据添加了噪声

分割数据

从上一章,您可能记得数据集通常分为三部分:训练验证测试。为了评估我们训练的模型的准确性,我们需要将其预测与真实数据进行比较,并检查它们的匹配程度。

这种评估发生在训练期间(称为验证)和训练之后(称为测试)。在每种情况下,使用的数据都必须是新鲜的,不能已经用于训练模型。

为了确保我们有数据用于评估,我们将在开始训练之前留出一些数据。让我们将我们的数据的 20%保留用于验证,另外 20%用于测试。我们将使用剩下的 60%来训练模型。这是训练模型时常用的典型分割。

以下代码分割我们的数据,然后将每个集合绘制为不同的颜色:

# We'll use 60% of our data for training and 20% for testing. The remaining 20%
# will be used for validation. Calculate the indices of each section.
TRAIN_SPLIT =  int(0.6 * SAMPLES)
TEST_SPLIT = int(0.2 * SAMPLES + TRAIN_SPLIT)

# Use np.split to chop our data into three parts.
# The second argument to np.split is an array of indices where the data will be
# split. We provide two indices, so the data will be divided into three chunks.
x_train, x_validate, x_test = np.split(x_values, [TRAIN_SPLIT, TEST_SPLIT])
y_train, y_validate, y_test = np.split(y_values, [TRAIN_SPLIT, TEST_SPLIT])

# Double check that our splits add up correctly
assert (x_train.size + x_validate.size + x_test.size) ==  SAMPLES

# Plot the data in each partition in different colors:
plt.plot(x_train, y_train, 'b.', label="Train")
plt.plot(x_validate, y_validate, 'y.', label="Validate")
plt.plot(x_test, y_test, 'r.', label="Test")
plt.legend()
plt.show()

为了分割我们的数据,我们使用另一个方便的 NumPy 方法:split()。这个方法接受一个数据数组和一个索引数组,然后在提供的索引处将数据分割成部分。

运行此单元格以查看我们分割的结果。每种类型的数据将由不同的颜色表示(或者如果您正在阅读本书的打印版本,则为不同的阴影),如图 4-11 所示。

我们的数据分为训练、验证和测试集的图

图 4-11。我们的数据分为训练、验证和测试集

定义基本模型

现在我们有了数据,是时候创建我们将训练以适应它的模型了。

我们将构建一个模型,该模型将接受一个输入值(在本例中为x)并使用它来预测一个数值输出值(x的正弦)。这种类型的问题称为回归。我们可以使用回归模型来处理各种需要数值输出的任务。例如,回归模型可以尝试根据来自加速度计的数据预测一个人的每小时英里数。

为了创建我们的模型,我们将设计一个简单的神经网络。它使用神经元层来尝试学习训练数据中的任何模式,以便进行预测。

实际上,执行此操作的代码非常简单。它使用Keras,TensorFlow 的用于创建深度学习网络的高级 API:

# We'll use Keras to create a simple model architecture
from tf.keras import layers
model_1 = tf.keras.Sequential()

# First layer takes a scalar input and feeds it through 16 "neurons." The
# neurons decide whether to activate based on the 'relu' activation function.
model_1.add(layers.Dense(16, activation='relu', input_shape=(1,)))

# Final layer is a single neuron, since we want to output a single value
model_1.add(layers.Dense(1))

# Compile the model using a standard optimizer and loss function for regression
model_1.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])

# Print a summary of the model's architecture
model_1.summary()

首先,我们使用 Keras 创建一个Sequential模型,这意味着每个神经元层都堆叠在下一个层上,就像我们在图 3-1 中看到的那样。然后我们定义两个层。这是第一层的定义:

model_1.add(layers.Dense(16, activation='relu', input_shape=(1,)))

第一层有一个单一的输入—我们的x值—和 16 个神经元。这是一个Dense层(也称为全连接层),意味着在推断时,当我们进行预测时,输入将被馈送到每一个神经元中。然后每个神经元将以一定程度被激活。每个神经元的激活程度基于其在训练期间学习到的权重偏差值,以及其激活函数。神经元的激活作为一个数字输出。

激活是通过一个简单的公式计算的,用 Python 显示。我们永远不需要自己编写这个代码,因为它由 Keras 和 TensorFlow 处理,但随着我们深入学习,了解这个公式将会很有帮助:

activation = activation_function((input * weight) + bias)

为了计算神经元的激活,它的输入被权重相乘,偏差被加到结果中。计算出的值被传递到激活函数中。得到的数字是神经元的激活。

激活函数是用于塑造神经元输出的数学函数。在我们的网络中,我们使用了一个称为修正线性单元ReLU的激活函数。这在 Keras 中由参数activation=relu指定。

ReLU 是一个简单的函数,在 Python 中显示如下:

def relu(input):
    return max(0.0, input)

ReLU 返回较大的值:它的输入或零。如果输入值为负,则 ReLU 返回零。如果输入值大于零,则 ReLU 返回不变。

图 4-12 显示了一系列输入值的 ReLU 输出。

从-10 到 10 的输入的 ReLU 图

图 4-12。从-10 到 10 的输入的 ReLU 图

没有激活函数,神经元的输出将始终是其输入的线性函数。这意味着网络只能模拟xy之间的比率在整个值范围内保持不变的线性关系。这将阻止网络对我们的正弦波进行建模,因为正弦波是非线性的。

由于 ReLU 是非线性的,它允许多层神经元联合起来模拟复杂的非线性关系,其中y值并不是每个x增量都增加相同的量。

注意

还有其他激活函数,但 ReLU 是最常用的。您可以在Wikipedia 关于激活函数的文章中看到其他选项。每个激活函数都有不同的权衡,机器学习工程师会进行实验,找出哪些选项对于给定的架构最有效。

来自我们第一层的激活数字将作为输入传递给我们的第二层,该层在以下行中定义:

model_1.add(layers.Dense(1))

因为这一层是一个单个神经元,它将接收 16 个输入,每个输入对应前一层中的一个神经元。它的目的是将前一层的所有激活组合成一个单一的输出值。由于这是我们的输出层,我们不指定激活函数,我们只想要原始结果。

因为这个神经元有多个输入,所以它有对应的每个输入的权重值。神经元的输出是通过以下公式计算的,如 Python 中所示:

# Here, `inputs` and `weights` are both NumPy arrays with 16 elements each
output = sum((inputs * weights)) + bias

输出值是通过将每个输入与其对应的权重相乘,对结果求和,然后加上神经元的偏差来获得的。

网络的权重和偏差在训练期间学习。在本章前面显示的代码中的compile()步骤配置了一些在训练过程中使用的重要参数,并准备好模型进行训练:

model_1.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])

optimizer参数指定了在训练期间调整网络以模拟其输入的算法。有几种选择,找到最佳选择通常归结为实验。您可以在Keras 文档中了解选项。

loss参数指定了在训练期间使用的方法,用于计算网络预测与现实之间的距离。这种方法称为损失函数。在这里,我们使用mse,或均方误差。这种损失函数用于回归问题,我们试图预测一个数字。Keras 中有各种损失函数可用。您可以在Keras 文档中看到一些选项。

metrics参数允许我们指定一些额外的函数,用于评估我们模型的性能。我们指定mae,或平均绝对误差,这是一个有用的函数,用于衡量回归模型的性能。这个度量将在训练期间进行测量,我们将在训练结束后获得结果。

在编译模型后,我们可以使用以下行打印关于其架构的一些摘要信息:

# Print a summary of the model's architecture
model_1.summary()

在 Colab 中运行单元格以定义模型。您将看到以下输出打印:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense (Dense)                (None, 16)                32
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 17
=================================================================
Total params: 49
Trainable params: 49
Non-trainable params: 0
_________________________________________________________________

这个表格显示了网络的层、它们的输出形状以及它们的参数数量。网络的大小——它占用的内存量——主要取决于它的参数数量,即其总权重和偏差的数量。在讨论模型大小和复杂性时,这可能是一个有用的指标。

对于像我们这样简单的模型,权重的数量可以通过计算模型中神经元之间的连接数来确定,假设每个连接都有一个权重。

我们刚刚设计的网络由两层组成。我们的第一层有 16 个连接——一个连接到每个神经元的输入。我们的第二层有一个神经元,也有 16 个连接——一个连接到第一层的每个神经元。这使得连接的总数为 32。

由于每个神经元都有一个偏差,网络有 17 个偏差,这意味着它总共有 32 + 17 = 49 个参数。

我们现在已经走完了定义我们模型的代码。接下来,我们将开始训练过程。

训练我们的模型

定义了我们的模型之后,就是训练它,然后评估其性能,看看它的工作效果如何。当我们看到指标时,我们可以决定是否足够好,或者是否应该对设计进行更改并重新训练。

在 Keras 中训练模型,我们只需调用其fit()方法,传递所有数据和一些其他重要参数。下一个单元格中的代码显示了如何:

history_1 = model_1.fit(x_train, y_train, epochs=1000, batch_size=16,
                     validation_data=(x_validate, y_validate))

运行单元格中的代码开始训练。您将看到一些日志开始出现:

Train on 600 samples, validate on 200 samples
Epoch 1/1000
600/600 [==============================] - 1s 1ms/sample - loss: 0.7887 - mae: 0.7848 - val_loss: 0.5824 - val_mae: 0.6867
Epoch 2/1000
600/600 [==============================] - 0s 155us/sample - loss: 0.4883 - mae: 0.6194 - val_loss: 0.4742 - val_mae: 0.6056

我们的模型现在正在训练。这将需要一些时间,所以在等待时,让我们详细了解我们对fit()的调用:

history_1 = model_1.fit(x_train, y_train, epochs=1000, batch_size=16,
                     validation_data=(x_validate, y_validate))

首先,您会注意到我们将fit()调用的返回值分配给一个名为history_1的变量。这个变量包含了关于我们训练运行的大量信息,我们稍后将使用它来调查事情的进展。

接下来,让我们看一下fit()函数的参数:

x_trainy_train

fit()的前两个参数是我们训练数据的xy值。请记住,我们的数据的部分被保留用于验证和测试,因此只有训练集用于训练网络。

epochs

下一个参数指定在训练期间整个训练集将通过网络运行多少次。时期越多,训练就越多。您可能会认为训练次数越多,网络就会越好。然而,一些网络在一定数量的时期后会开始过拟合其训练数据,因此我们可能希望限制我们进行的训练量。

此外,即使没有过拟合,网络在一定数量的训练后也会停止改进。由于训练需要时间和计算资源,最好不要在网络没有变得更好的情况下进行训练!

我们开始使用 1,000 个时期进行训练。训练完成后,我们可以深入研究我们的指标,以发现这是否是正确的数量。

batch_size

batch_size参数指定在测量准确性并更新权重和偏差之前要向网络提供多少训练数据。如果需要,我们可以指定batch_size1,这意味着我们将在单个数据点上运行推断,测量网络预测的损失,更新权重和偏差以使下次预测更准确,然后继续这个循环直到处理完所有数据。

因为我们有 600 个数据点,每个时期会导致网络更新 600 次。这是很多计算量,所以我们的训练会花费很长时间!另一种选择可能是选择并对多个数据点运行推断,测量总体损失,然后相应地更新网络。

如果将batch_size设置为600,每个批次将包括所有训练数据。现在,我们每个时代只需要对网络进行一次更新,速度更快。问题是,这会导致模型的准确性降低。研究表明,使用大批量大小训练的模型对新数据的泛化能力较差,更容易过拟合。

妥协的方法是使用一个介于中间的批量大小。在我们的训练代码中,我们使用批量大小为 16。这意味着我们会随机选择 16 个数据点,对它们进行推断,计算总体损失,并每批次更新一次网络。如果我们有 600 个训练数据点,网络将在每个时代更新大约 38 次,这比 600 次要好得多。

在选择批量大小时,我们在训练效率和模型准确性之间做出妥协。理想的批量大小会因模型而异。最好从批量大小为 16 或 32 开始,并进行实验以找出最佳工作方式。

验证数据

这是我们指定验证数据集的地方。来自该数据集的数据将在整个训练过程中通过网络运行,并且网络的预测将与预期值进行比较。我们将在日志中看到验证结果,并作为history_1对象的一部分。

训练指标

希望到目前为止,培训已经结束。如果没有,请等待一段时间以完成培训。

我们现在将检查各种指标,以查看我们的网络学习情况如何。首先,让我们查看训练期间编写的日志。这将显示网络如何从其随机初始状态改进。

这是我们第一个和最后一个时代的日志:

Epoch 1/1000
600/600 [==============================] - 1s 1ms/sample - loss: 0.7887 - mae: 0.7848 - val_loss: 0.5824 - val_mae: 0.6867
Epoch 1000/1000
600/600 [==============================] - 0s 124us/sample - loss: 0.1524 - mae: 0.3039 - val_loss: 0.1737 - val_mae: 0.3249

损失maeval_lossval_mae告诉我们各种事情:

损失

这是我们损失函数的输出。我们使用均方误差,它表示为正数。通常,损失值越小,越好,因此在评估网络时观察这一点是一个好方法。

比较第一个和最后一个时代,网络在训练过程中显然有所改进,从约 0.7 的损失到更小的约 0.15。让我们看看其他数字,以确定这种改进是否足够!

mae

这是我们训练数据的平均绝对误差。它显示了网络预测值与训练数据中预期y值之间的平均差异。

可以预期我们的初始误差会非常糟糕,因为它基于未经训练的网络。这当然是事实:网络的预测平均偏差约为 0.78,这是一个很大的数字,当可接受值的范围仅为-1 到 1 时!

然而,即使在训练之后,我们的平均绝对误差仍然约为 0.30。这意味着我们的预测平均偏差约为 0.30,这仍然相当糟糕。

val_loss

这是我们验证数据上损失函数的输出。在我们的最后一个时代中,训练损失(约 0.15)略低于验证损失(约 0.17)。这暗示我们的网络可能存在过拟合问题,因为它在未见过的数据上表现更差。

val_mae

这是我们验证数据的平均绝对误差。值为约 0.32,比我们训练集上的平均绝对误差更糟糕,这是网络可能存在过拟合的另一个迹象。

绘制历史数据

到目前为止,很明显我们的模型并没有做出准确的预测。我们现在的任务是找出原因。为此,让我们利用我们history_1对象中收集的数据。

下一个单元格从历史对象中提取训练和验证损失数据,并将其绘制在图表上:

loss = history_1.history['loss']
val_loss = history_1.history['val_loss']

epochs = range(1, len(loss) + 1)

plt.plot(epochs, loss, 'g.', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

history_1对象包含一个名为history_1.history的属性,这是一个记录训练和验证期间指标值的字典。我们使用这个来收集我们要绘制的数据。对于我们的 x 轴,我们使用时期数,通过查看损失数据点的数量来确定。运行单元格,您将在图 4-13 中看到图形。

训练和验证损失的图形

图 4-13。训练和验证损失的图形

正如您所看到的,损失量在前 50 个时期内迅速减少,然后趋于稳定。这意味着模型正在改进并产生更准确的预测。

我们的目标是在模型不再改进或训练损失小于验证损失时停止训练,这意味着模型已经学会如此好地预测训练数据,以至于无法推广到新数据。

损失在最初几个时期急剧下降,这使得其余的图表非常难以阅读。让我们通过运行下一个单元格来跳过前 100 个时期:

# Exclude the first few epochs so the graph is easier to read
SKIP = 100

plt.plot(epochs[SKIP:], loss[SKIP:], 'g.', label='Training loss')
plt.plot(epochs[SKIP:], val_loss[SKIP:], 'b.', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

图 4-14 展示了此单元格生成的图形。

跳过前 100 个时期的训练和验证损失图

图 4-14。跳过前 100 个时期的训练和验证损失图

现在我们已经放大了,您可以看到损失继续减少直到大约 600 个时期,此时它基本稳定。这意味着可能没有必要训练我们的网络那么长时间。

但是,您还可以看到最低的损失值仍然约为 0.15。这似乎相对较高。此外,验证损失值始终更高。

为了更深入地了解我们模型的性能,我们可以绘制更多数据。这次,让我们绘制平均绝对误差。运行下一个单元格来执行:

# Draw a graph of mean absolute error, which is another way of
# measuring the amount of error in the prediction.
mae = history_1.history['mae']
val_mae = history_1.history['val_mae']

plt.plot(epochs[SKIP:], mae[SKIP:], 'g.', label='Training MAE')
plt.plot(epochs[SKIP:], val_mae[SKIP:], 'b.', label='Validation MAE')
plt.title('Training and validation mean absolute error')
plt.xlabel('Epochs')
plt.ylabel('MAE')
plt.legend()
plt.show()

图 4-15 显示了结果图形。

训练和验证期间的平均绝对误差图

图 4-15。训练和验证期间的平均绝对误差图

这个平均绝对误差图给了我们一些进一步的线索。我们可以看到,平均而言,训练数据显示的误差比验证数据低,这意味着网络可能已经过拟合,或者学习了训练数据,以至于无法对新数据做出有效预测。

此外,平均绝对误差值相当高,约为 0.31 左右,这意味着模型的一些预测至少有 0.31 的错误。由于我们的预期值的范围仅为-1 到+1,0.31 的误差意味着我们离准确建模正弦波还有很大距离。

为了更深入了解发生了什么,我们可以将网络对训练数据的预测与预期值绘制在一起。

这发生在以下单元格中:

# Use the model to make predictions from our validation data
predictions = model_1.predict(x_train)

# Plot the predictions along with the test data
plt.clf()
plt.title('Training data predicted vs actual values')
plt.plot(x_test, y_test, 'b.', label='Actual')
plt.plot(x_train, predictions, 'r.', label='Predicted')
plt.legend()
plt.show()

通过调用model_1.predict(x_train),我们对训练数据中的所有x值进行推断。该方法返回一个预测数组。让我们将这个绘制在图上,与我们训练集中的实际y值一起。运行单元格,您将在图 4-16 中看到图形。

我们训练数据的预测与实际值的图形

图 4-16。我们训练数据的预测与实际值的图形

哦,亲爱的!图表清楚地表明我们的网络已经学会以非常有限的方式逼近正弦函数。预测非常线性,只是非常粗略地拟合数据。

这种拟合的刚性表明模型没有足够的容量来学习正弦波函数的全部复杂性,因此它只能以过于简单的方式逼近它。通过使我们的模型更大,我们应该能够提高其性能。

改进我们的模型

凭借我们的原始模型太小无法学习数据的复杂性的知识,我们可以尝试改进它。这是机器学习工作流程的正常部分:设计模型,评估其性能,并进行更改,希望看到改进。

扩大网络的简单方法是添加另一层神经元。每一层神经元代表输入的转换,希望能使其更接近预期的输出。网络有更多层神经元,这些转换就可以更复杂。

运行以下单元格以重新定义我们的模型,方式与之前相同,但在中间增加了 16 个神经元的额外层:

model_2 = tf.keras.Sequential()

# First layer takes a scalar input and feeds it through 16 "neurons." The
# neurons decide whether to activate based on the 'relu' activation function.
model_2.add(layers.Dense(16, activation='relu', input_shape=(1,)))

# The new second layer may help the network learn more complex representations
model_2.add(layers.Dense(16, activation='relu'))

# Final layer is a single neuron, since we want to output a single value
model_2.add(layers.Dense(1))

# Compile the model using a standard optimizer and loss function for regression
model_2.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])

# Show a summary of the model
model_2.summary()

正如您所看到的,代码基本上与我们第一个模型相同,但增加了一个Dense层。让我们运行这个单元格来查看summary()结果:

Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
dense_2 (Dense)              (None, 16)                32
_________________________________________________________________
dense_3 (Dense)              (None, 16)                272
_________________________________________________________________
dense_4 (Dense)              (None, 1)                 17
=================================================================
Total params: 321
Trainable params: 321
Non-trainable params: 0
_________________________________________________________________

有了 16 个神经元的两层,我们的新模型要大得多。它有(1 * 16) + (16 * 16) + (16 * 1) = 288 个权重,加上 16 + 16 + 1 = 33 个偏差,总共是 288 + 33 = 321 个参数。我们的原始模型只有 49 个总参数,因此模型大小增加了 555%。希望这种额外的容量将有助于表示数据的复杂性。

接下来的单元格将训练我们的新模型。由于我们的第一个模型改进得太快,这次让我们训练更少的时代——只有 600 个。运行这个单元格开始训练:

history_2 = model_2.fit(x_train, y_train, epochs=600, batch_size=16,
                     validation_data=(x_validate, y_validate))

训练完成后,我们可以查看最终日志,快速了解事情是否有所改善:

Epoch 600/600
600/600 [==============================] - 0s 150us/sample - loss: 0.0115 - mae: 0.0859 - val_loss: 0.0104 - val_mae: 0.0806

哇!您可以看到我们已经取得了巨大的进步——验证损失从 0.17 降至 0.01,验证平均绝对误差从 0.32 降至 0.08。这看起来非常有希望。

为了了解情况如何,让我们运行下一个单元格。它设置为生成我们上次使用的相同图表。首先,我们绘制损失的图表:

# Draw a graph of the loss, which is the distance between
# the predicted and actual values during training and validation.
loss = history_2.history['loss']
val_loss = history_2.history['val_loss']

epochs = range(1, len(loss) + 1)

plt.plot(epochs, loss, 'g.', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

图 4-17 显示了结果。

接下来,我们绘制相同的损失图,但跳过前 100 个时代,以便更好地看到细节:

# Exclude the first few epochs so the graph is easier to read
SKIP = 100

plt.clf()

plt.plot(epochs[SKIP:], loss[SKIP:], 'g.', label='Training loss')
plt.plot(epochs[SKIP:], val_loss[SKIP:], 'b.', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

训练和验证损失的图表

图 4-17。训练和验证损失的图表

图 4-18 展示了输出。

最后,我们绘制相同一组时代的平均绝对误差:

plt.clf()

# Draw a graph of mean absolute error, which is another way of
# measuring the amount of error in the prediction.
mae = history_2.history['mae']
val_mae = history_2.history['val_mae']

plt.plot(epochs[SKIP:], mae[SKIP:], 'g.', label='Training MAE')
plt.plot(epochs[SKIP:], val_mae[SKIP:], 'b.', label='Validation MAE')
plt.title('Training and validation mean absolute error')
plt.xlabel('Epochs')
plt.ylabel('MAE')
plt.legend()
plt.show()

训练和验证损失的图表,跳过前 100 个时代

图 4-18。训练和验证损失的图表,跳过前 100 个时代

图 4-19 描述了图表。

训练和验证期间的平均绝对误差图

图 4-19。训练和验证期间的平均绝对误差图

很棒的结果!从这些图表中,我们可以看到两个令人兴奋的事情:

  • 验证的指标比训练的要好,这意味着网络没有过拟合。

  • 总体损失和平均绝对误差比我们之前的网络要好得多。

您可能想知道为什么验证的指标比训练的好,而不仅仅是相同的。原因是验证指标是在每个时代结束时计算的,而训练指标是在训练时代仍在进行时计算的。这意味着验证是在一个训练时间稍长的模型上进行的。

根据我们的验证数据,我们的模型似乎表现很好。然而,为了确保这一点,我们需要进行最后一次测试。

测试

之前,我们留出了 20%的数据用于测试。正如我们讨论过的,拥有单独的验证和测试数据非常重要。由于我们根据验证性能微调我们的网络,存在一个风险,即我们可能会意外地调整模型以过度拟合其验证集,并且可能无法推广到新数据。通过保留一些新鲜数据并将其用于对模型的最终测试,我们可以确保这种情况没有发生。

在使用了我们的测试数据之后,我们需要抵制进一步调整模型的冲动。如果我们为了提高测试性能而进行更改,可能会导致过拟合测试集。如果这样做了,我们将无法知道,因为我们没有剩余的新数据来进行测试。

这意味着如果我们的模型在测试数据上表现不佳,那么是时候重新考虑了。我们需要停止优化当前模型,并提出全新的架构。

考虑到这一点,接下来的单元将评估我们的模型与测试数据的表现:

# Calculate and print the loss on our test dataset
loss = model_2.evaluate(x_test, y_test)

# Make predictions based on our test dataset
predictions = model_2.predict(x_test)

# Graph the predictions against the actual values
plt.clf()
plt.title('Comparison of predictions and actual values')
plt.plot(x_test, y_test, 'b.', label='Actual')
plt.plot(x_test, predictions, 'r.', label='Predicted')
plt.legend()
plt.show()

首先,我们使用测试数据调用模型的evaluate()方法。这将计算并打印损失和平均绝对误差指标,告诉我们模型的预测与实际值的偏差有多大。接下来,我们进行一组预测,并将其与实际值一起绘制在图表上。

现在我们可以运行单元,了解我们的模型表现如何!首先,让我们看看evaluate()的结果:

200/200 [==============================] - 0s 71us/sample - loss: 0.0103 - mae: 0.0718

这显示有 200 个数据点被评估,这是我们整个测试集。模型每次预测需要 71 微秒。损失指标为 0.0103,非常出色,并且非常接近我们的验证损失 0.0104。我们的平均绝对误差为 0.0718,也非常小,与验证中的 0.0806 相当接近。

这意味着我们的模型运行良好,没有过拟合!如果模型过拟合了验证数据,我们可以预期测试集上的指标会明显比验证结果差。

我们的预测与实际值的图表,显示在图 4-20 中,清楚地展示了我们的模型表现如何。

我们的测试数据的预测与实际值的图表

图 4-20。我们的测试数据的预测与实际值的图表

你可以看到,大部分情况下,代表预测值的点形成了一个平滑的曲线,沿着实际值的分布中心。我们的网络已经学会了近似正弦曲线,即使数据集很嘈杂!

然而,仔细观察,你会发现一些不完美之处。我们预测的正弦波的峰值和谷值并不完全平滑,像真正的正弦波那样。我们模型学习了训练数据的变化,这些数据是随机分布的。这是过拟合的轻微情况:我们的模型没有学习到平滑的正弦函数,而是学会了复制数据的确切形状。

对于我们的目的,这种过拟合并不是一个主要问题。我们的目标是让这个模型轻轻地控制 LED 的亮度,不需要完全平滑才能实现这一目标。如果我们认为过拟合的程度有问题,我们可以尝试通过正则化技术或获取更多的训练数据来解决。

现在我们对模型满意了,让我们准备在设备上部署它!

将模型转换为 TensorFlow Lite

在本章的开头,我们简要提到了 TensorFlow Lite,这是一组用于在“边缘设备”上运行 TensorFlow 模型的工具。

第十三章详细介绍了用于微控制器的 TensorFlow Lite。目前,我们可以将其视为具有两个主要组件:

TensorFlow Lite 转换器

这将 TensorFlow 模型转换为一种特殊的、节省空间的格式,以便在内存受限设备上使用,并且可以应用优化,进一步减小模型大小,并使其在小型设备上运行更快。

TensorFlow Lite 解释器

这将使用给定设备的最有效操作来运行适当转换为 TensorFlow Lite 模型。

在使用 TensorFlow Lite 之前,我们需要将模型转换。我们使用 TensorFlow Lite 转换器的 Python API 来完成这个任务。它将我们的 Keras 模型写入磁盘,以FlatBuffer的形式,这是一种专门设计的节省空间的文件格式。由于我们要部署到内存有限的设备,这将非常有用!我们将在第十二章中更详细地了解 FlatBuffers。

除了创建 FlatBuffer 外,TensorFlow Lite 转换器还可以对模型应用优化。这些优化通常会减小模型的大小、运行时间,或者两者兼而有之。这可能会导致准确度降低,但降低通常是小到足以值得的。您可以在第十三章中了解更多关于优化的信息。

最有用的优化之一是量化。默认情况下,模型中的权重和偏置以 32 位浮点数存储,以便在训练期间进行高精度计算。量化允许您减少这些数字的精度,使其适合于 8 位整数——大小减小四倍。更好的是,因为 CPU 更容易使用整数而不是浮点数进行数学运算,量化模型将运行得更快。

量化最酷的一点是,它通常会导致准确度的最小损失。这意味着在部署到低内存设备时,几乎总是值得的。

在下一个单元格中,我们使用转换器创建并保存我们模型的两个新版本。第一个转换为 TensorFlow Lite FlatBuffer 格式,但没有任何优化。第二个是量化的。

运行单元格将模型转换为这两种变体:

# Convert the model to the TensorFlow Lite format without quantization
converter = tf.lite.TFLiteConverter.from_keras_model(model_2)
tflite_model = converter.convert()

# Save the model to disk
open("sine_model.tflite," "wb").write(tflite_model)

# Convert the model to the TensorFlow Lite format with quantization
converter = tf.lite.TFLiteConverter.from_keras_model(model_2)
# Indicate that we want to perform the default optimizations,
# which include quantization
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# Define a generator function that provides our test data's x values
# as a representative dataset, and tell the converter to use it
def representative_dataset_generator():
  for value in x_test:
    # Each scalar value must be inside of a 2D array that is wrapped in a list
    yield [np.array(value, dtype=np.float32, ndmin=2)]
converter.representative_dataset = representative_dataset_generator
# Convert the model
tflite_model = converter.convert()

# Save the model to disk
open("sine_model_quantized.tflite," "wb").write(tflite_model)

为了创建一个尽可能高效运行的量化模型,我们需要提供一个代表性数据集——一组数字,代表了模型训练时数据集的全部输入值范围。

在前面的单元格中,我们可以使用测试数据集的x值作为代表性数据集。我们定义一个函数representative_dataset_generator(),使用yield操作符逐个返回这些值。

为了证明这些模型在转换和量化后仍然准确,我们使用它们进行预测,并将结果与我们的测试结果进行比较。鉴于这些是 TensorFlow Lite 模型,我们需要使用 TensorFlow Lite 解释器来执行此操作。

由于 TensorFlow Lite 解释器主要设计用于效率,因此使用起来比 Keras API 稍微复杂一些。要使用我们的 Keras 模型进行预测,我们只需调用predict()方法,传递一个输入数组即可。而对于 TensorFlow Lite,我们需要执行以下操作:

  1. 实例化一个Interpreter对象。

  2. 调用一些为模型分配内存的方法。

  3. 将输入写入输入张量。

  4. 调用模型。

  5. 从输出张量中读取输出。

这听起来很多,但现在不要太担心;我们将在第五章中详细介绍。现在,运行以下单元格,使用两个模型进行预测,并将它们与原始未转换的模型的结果一起绘制在图表上:

# Instantiate an interpreter for each model
sine_model = tf.lite.Interpreter('sine_model.tflite')
sine_model_quantized = tf.lite.Interpreter('sine_model_quantized.tflite')

# Allocate memory for each model
sine_model.allocate_tensors()
sine_model_quantized.allocate_tensors()

# Get indexes of the input and output tensors
sine_model_input_index = sine_model.get_input_details()[0]["index"]
sine_model_output_index = sine_model.get_output_details()[0]["index"]
sine_model_quantized_input_index = sine_model_quantized.get_input_details()[0]["index"]
sine_model_quantized_output_index = \
  sine_model_quantized.get_output_details()[0]["index"]

# Create arrays to store the results
sine_model_predictions = []
sine_model_quantized_predictions = []

# Run each model's interpreter for each value and store the results in arrays
for x_value in x_test:
  # Create a 2D tensor wrapping the current x value
  x_value_tensor = tf.convert_to_tensor([[x_value]], dtype=np.float32)
  # Write the value to the input tensor
  sine_model.set_tensor(sine_model_input_index, x_value_tensor)
  # Run inference
  sine_model.invoke()
  # Read the prediction from the output tensor
  sine_model_predictions.append(
      sine_model.get_tensor(sine_model_output_index)[0])
  # Do the same for the quantized model
  sine_model_quantized.set_tensor\
  (sine_model_quantized_input_index, x_value_tensor)
  sine_model_quantized.invoke()
  sine_model_quantized_predictions.append(
      sine_model_quantized.get_tensor(sine_model_quantized_output_index)[0])

# See how they line up with the data
plt.clf()
plt.title('Comparison of various models against actual values')
plt.plot(x_test, y_test, 'bo', label='Actual')
plt.plot(x_test, predictions, 'ro', label='Original predictions')
plt.plot(x_test, sine_model_predictions, 'bx', label='Lite predictions')
plt.plot(x_test, sine_model_quantized_predictions, 'gx', \
  label='Lite quantized predictions')
plt.legend()
plt.show()

运行此单元格将产生图 4-21 中的图表。

比较模型预测与实际值的图表

图 4-21。比较模型预测与实际值的图表

从图表中我们可以看到,原始模型、转换模型和量化模型的预测都非常接近,几乎无法区分。情况看起来很不错!

由于量化使模型变小,让我们比较两个转换后的模型,看看大小上的差异。运行以下单元格计算它们的大小并进行比较:

import os
basic_model_size = os.path.getsize("sine_model.tflite")
print("Basic model is %d bytes" % basic_model_size)
quantized_model_size = os.path.getsize("sine_model_quantized.tflite")
print("Quantized model is %d bytes" % quantized_model_size)
difference = basic_model_size - quantized_model_size
print("Difference is %d bytes" % difference)

您应该看到以下输出:

Basic model is 2736 bytes
Quantized model is 2512 bytes
Difference is 224 bytes

我们的量化模型比原始版本小 224 字节,这很好,但大小只有轻微减小。在约 2.4 KB 左右,这个模型已经非常小,权重和偏差只占整体大小的一小部分。除了权重,模型还包含构成我们深度学习网络架构的所有逻辑,称为计算图。对于真正微小的模型,这可能比模型的权重占用更多的空间,这意味着量化几乎没有效果。

更复杂的模型有更多的权重,这意味着量化带来的空间节省将更高。对于大多数复杂模型,可以预期接近四倍。

无论其确切大小如何,我们的量化模型执行起来都比原始版本快,这对于微小微控制器非常重要。

转换为 C 文件

为了让我们的模型能够与 TensorFlow Lite for Microcontrollers 一起使用的最后一步是将其转换为一个可以包含在我们应用程序中的 C 源文件。

在本章中,我们一直在使用 TensorFlow Lite 的 Python API。这意味着我们可以使用Interpreter构造函数从磁盘加载我们的模型文件。

然而,大多数微控制器没有文件系统,即使有,从磁盘加载模型所需的额外代码也会在有限的空间下是浪费的。相反,作为一个优雅的解决方案,我们提供了一个可以包含在我们的二进制文件中并直接加载到内存中的 C 源文件中的模型。

在文件中,模型被定义为一个字节数组。幸运的是,有一个方便的 Unix 工具名为xxd,能够将给定文件转换为所需的格式。

以下单元格在我们的量化模型上运行xxd,将输出写入名为sine_model_quantized.cc的文件,并将其打印到屏幕上:

# Install xxd if it is not available
!apt-get -qq install xxd
# Save the file as a C source file
!xxd -i sine_model_quantized.tflite > sine_model_quantized.cc
# Print the source file
!cat sine_model_quantized.cc

输出非常长,所以我们不会在这里全部复制,但这里有一个片段,包括开头和结尾:

unsigned char sine_model_quantized_tflite[] = {
  0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x00, 0x00, 0x12, 0x00,
  0x1c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x14, 0x00,
  // ...
  0x00, 0x00, 0x08, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
  0x04, 0x00, 0x00, 0x00
};
unsigned int sine_model_quantized_tflite_len = 2512;

要在项目中使用这个模型,您可以复制粘贴源代码,或者从笔记本中下载文件。

总结

有了这个,我们构建我们的模型就完成了。我们已经训练、评估并转换了一个 TensorFlow 深度学习网络,可以接收 0 到 2π之间的数字,并输出其正弦的良好近似值。

这是我们第一次使用 Keras 训练微小模型。在未来的项目中,我们将训练仍然微小但远远更复杂的模型。

现在,让我们继续第五章,在那里我们将编写代码在微控制器上运行我们的模型。