跳到主要内容

数据大到读不进来 — 让它一条一条流过去

这一章讲三件事: 数据大到内存装不下时该怎么办; 「一条流」和「一个数组」在用法上到底差在哪; 以及浏览器里跑深度学习必须自己记的一笔账——显存不会自动回收它在全书链条里的位置: 前七章所有数据都是洗干净、一次读得进内存的。 从这一章起,书开始讲现实。 这一章管「数据怎么进来」, 第 09 章管「进来的数据本身是歪的」——两件事,两章。 顺带说一句:这两章在母本《Python深度学习》里一个字都没有,是这本书独有的部分。

1. 先看现象:两个 fit() 干不了的活

书开篇给了两个问题1:

  • 你有一个几百 GB 的邮件库,要训一个垃圾邮件过滤器。怎么把它读进内存?
  • 你的训练图库一台机器都装不下怎么训?

前七章的做法一律是:把数据整个变成张量,交给 model.fit(x, y)这个做法有一条硬上限。

model.fit(images, targets)

这个 images 必须整个待在显存里

第 07 章那个目标检测的例子:2000 张 224×224 的彩图
如果换成 25 万张呢?

样例数 × 高 × 宽 × 颜色 × 每个数占几个字节
= 250 000 × 224 × 224 × 3 × 4
≈ 150 GB

图说:这是书自己算的一笔账。2019 年没有哪个浏览器能给你 150GB。
而且显存通常比系统内存还小 —— JavaScript 引擎本身在 64 位系统上就有 1.4GB 的上限。

还有一条更隐蔽的账:代码会烂。 书说得很实在: 没有「数据集」这层抽象,底层数据源的实现细节非常容易和训练代码混在一起; 一旦数据源变了,这部分代码将成为一团乱麻2

2. 顶层全景:声明的时候什么都不做

const ds = tf.data.csv(url) ← 此刻:零个网络请求,零个字节
.skip(300) ← 此刻:仍然零个请求
.take(1) ← 此刻:还是零个
.map(preprocess) ← 还是零个

await ds.toArray() ← 只有到这一句,才真的去拿数据
而且只拿它需要的那么多

图说:前面四行不是在搬数据,是在**写一份说明书**。
书的原话是:可以把这样一条链看成「一个小程序」,只有在链尾真的要用某个元素时才会运行。

这种「不到最后一刻不干活」的做法就叫惰性。 下一节整节讲它。

3. 承重节:惰性流

这一节是本章的地基。它一句话讲得完,但它改变了你写代码的方式。

它是什么

tf.data.Dataset 是这一章的主角。粗略地说,它是一个可以逐个取出元素的集合, 和 Node.js 里的流(Stream)很像3:

当程序要取下一个元素时,它才去下载、读取或者当场生成这个元素。3

「元素」在绝大多数情况下就是一个样例:训练集里是一对 (输入, 答案); 读 CSV 时是文件的一行4

它买到了什么

好处说明
能处理比内存大的数据因为任何时刻内存里只有一小批
能预取框架可以在你还在算这一批时,先把下一批准备好
代码不会烂换数据源只改一行,训练代码一个字不动

三种造法

书给了三个入口5:

① 现成的数组: tf.data.array([...])它不会让训练变快,也不省内存——数据本来就在内存里。 但它让你能用上后面那套链式方法和 fitDataset()

② CSV 文件(本地或远程): tf.data.csv(url)一行代码就是一条流。 每个元素是一个对象,属性名就是 CSV 的列名——你不必记住哪一列是第几列6

③ 生成器函数: tf.data.generator(fn),fn 是 JavaScript 的 function*这一种最灵活,而且它能造出无限多的样例。 书的例子是模拟掷两个骰子:

function* rollTwoDiceGeneratorFn() {
while (true) { yield rollTwoDice(); } // 永远不结束
}
const ds = tf.data.generator(rollTwoDiceGeneratorFn);
await ds.take(1).forEachAsync(e => console.log(e)); // 打印类似 [4, 2]

这个例子里藏着两个必须记住的点7:

  1. 这个数据集是无限的。 你要是直接 toArray()forEachAsync(), 它会一直跑到浏览器或服务器崩溃。所以必须先用 take(n) 截出有限的一段;
  2. 不取就不算。 书在生成器函数外面放了一个计数器, 取一个样例之后,计数器的值是 1——函数只被调用了一次

读出来的两种方式,都是异步的

异步在这里的意思是:这两个方法不会当场把结果交给你, 它们先返回一个「我以后会给你」的凭据,你得用 await 等它。

方法干什么危险在哪
.toArray()遍历整个数据集,全部塞进一个数组返回数据集很大或无限时会直接爆掉
.forEachAsync(f)遍历整个数据集,对每个元素执行 f同上

这两个都是异步的,而数组的 forEach 是同步的——这是个高频 bug 源8。 书点破了最常见的那一种:忘了 await,于是在 Promise 还没出结果时就去读结果, 误以为数据集是空的

为什么非得异步? 因为流里的数据常常要从远程下载或当场算出来; 异步能让等待的时间被利用起来8

4. 链式方法:你在写说明书,不是在搬数据

Dataset 提供一串可以接龙的方法,每一个都返回一个新的 Dataset9:

方法干什么
.filter(判定函数)只留下判定为真的元素
.map(转换函数)每个元素过一遍转换
.batch(n)把连续 n 个元素打包成一批,顺带把普通数值转成张量
.take(n) / .skip(n)只要前 n 个 / 跳过前 n 个
.repeat(n)整个数据集重复 n 遍(不给数就无限重复)
.shuffle(窗口大小, 种子)打乱顺序——但只在一个固定大小的窗口内打乱

shuffle 那一行的注意事项极其要紧,而且第 09 章整章都建立在它上面。 它只在窗口内打乱,窗口之外的顺序动不了:

tf.data.array([1,2,3,4,5,6]).shuffle(3)
可能得到: 2, 4, 1, 3, 6, 5

但是 —— 最后那个 6 永远不可能跑到第一个位置去,
因为它离窗口(大小 3)的覆盖范围太远了。

图说:窗口比数据集小,打乱就是局部的。
这个限制是流式处理的必然结果:框架不可能为了洗牌而先把无限的数据全读进来。

一个能验算的实验:顺序会影响算力

「链式调用只是在搭流水线」这句话听着抽象。书用一个计数器把它变成了可验证的10:

写法 A: 数据.skip(6).map(计数函数).forEachAsync(...)
计数器最终 = 0 ← 6 个元素全被跳过了,转换函数一次都没跑

写法 B: 数据.map(计数函数).skip(6).forEachAsync(...)
计数器最终 = 6 ← 6 个元素全被转换了一遍,然后才被扔掉

图说:两种写法结果完全一样,算力差了 6 次转换。
数据集越大、转换越贵,这个差距越吓人 —— 这就是「先跳过再转换」这条规矩的由来。

划分训练集和测试集:靠同一个随机种子

流没有「下标」可用,那怎么切出互不重叠的两份?书的办法很巧11:

const seed = Math.floor(Math.random() * 10000); // 一个随机种子

const trainData = tf.data.array(RAW).shuffle(RAW.length, seed).take(N).map(pre);
const testData = tf.data.array(RAW).shuffle(RAW.length, seed).skip(N).map(pre);

**两次用同一个种子** —— 于是两次打乱出的顺序一模一样

如果两次种子不同,两次打乱的结果就不一样,take(N)skip(N) 会挑出重叠的样例—— 那你的测试集里就混进了训练过的数据。 而且注意 .map(pre) 排在 take/skip 之后, 理由就是上面那个计数器实验。

流式标准化:一个绕不开的妥协

第 03 章那道标准化手续,在流上做不了了。因为你算不出均值。

书给的例子极其锋利12:

假设一个分布里几乎所有值都是 0,但每一千万个样例里有一个是 10 亿。 这个分布的真实均值是 100。可如果你只拿前一百万个样例去估,你会得到 0。

在无限的流上,「整个数据集的均值」根本不存在。 只能退而求其次: 一边走一边累计——用一个闭包记住「至今见过多少个、总和是多少」, 每来一个元素就拿当前的估计值去减13

这是一个妥协,不是一个解法。 书没有掩饰这一点,还额外提醒: 累计的那两个数有溢出的风险13

5. 训练入口:从 fit 换成 fitDataset

model.fitDataset(dataset, config) 要求数据集吐出来的每个元素长这样14:

{ xs: 一批特征的张量, ys: 一批答案的张量 }

比起 fit(),它多了两个必填的配置,而且这两个都是因为「流可能是无限的」才存在的15:

配置为什么必须有
batchesPerEpoch数组有长度,一轮跑完自己就知道了;流没有长度,所以你必须告诉框架「跑多少批算一轮」——否则每轮结束时的回调永远不会被触发
validationBatches同理:验证集如果也是无限流,不给这个数,框架会一直抽下去,程序卡死

一个能看清账的例子

书拿一个纸牌游戏演示:两个玩家各拿 N 张 1~13 的牌,比大小; 模型看玩家 1 的牌,预测他会不会赢。不看牌瞎猜的胜率正好是 50%16

数据全部是当场生成的(生成器函数),要多少有多少

50 轮 × 每轮 50 批 × 每批 100 个样例 = 25 万个样例
训完准确率约 75%

图说:25 万个样例,一次也没有整个待在内存里过。
对照第 1 节那笔账 —— 同样 25 万个样例,如果是 224×224 的图,要 150GB。

这一步是这一章的兑现:「数据大到读不进来」这个问题,到这里真的被解决了。

6. 主走查:波士顿房价 CSV,这次做成流

这一章的每个承重机制,在这条走查上各占一步。 ⚠ 第 3 步那条记录是书里第 6 章打印出来的真实记录;第 6 步的批尺寸是我们举的例。

我们拿第 03 章那份熟悉的数据,这次不再整个读进来。

发生了什么具体的数 / 状态
1tf.data.csv(url)零个网络请求,零个字节 —— 只是记下了「数据在哪」
2.skip(300).take(1)仍然零个请求 —— 只是又往说明书上加了两行
3.toArray()到这一句才真的发请求,拿回第 301 行:{crim: 0.3237, zn: 0, indus: 2.18, …, tax: 222, ptratio: 18.7, lstat: 2.94}
4查一下列名columnNames()只请求并解析头一行,返回 ["crim", "zn", "indus", …, "lstat"]
5告诉它哪一列是答案columnConfigs: {medv: {isLabel: true}},于是每个元素变成 {xs, ys} 两半
6.batch(128)连续 128 个元素打成一批,顺带把 JavaScript 数值转成张量 —— 形状 [128, 12][128, 1]
7model.fitDataset(ds, {batchesPerEpoch: …})必须给 batchesPerEpoch,因为流不会自己说「我到头了」
8训练开始框架一批一批地拉,任何时刻内存里只有一小批

第 1、2、3 步是这条走查的心脏:三行代码写完了,一个字节都还没读。

另起一处:摄像头那一帧

摄像头也是流,但它和 CSV 有两处根本不同17:

① 它的内容取决于**你什么时候取** —— CSV 你取快取慢都是同一行,摄像头不是
② 它是无限的,你不显式停掉它就一直开着

const webcam = await tf.data.webcam(videoElement);
const img = await webcam.capture(); ← 抓一帧,返回一个张量
// …… 用它推断 ……
tf.dispose(img); ← ⚠ 必须自己回收,理由见下一节
webcam.stop(); ← ⚠ 用完必须显式停

书特别点了三条反直觉的规矩:

  1. 不能对摄像头流用 forEachtoArray 因为 JavaScript 的执行速度远快于摄像头的帧率, 那样会造出一堆重复帧,白烧算力;正确做法是自己写循环,用 tf.nextFrame() 控制节奏18;
  2. 不要用 map() 做预处理。 摄像头是异步的,map 那套延后执行在这里会添乱; 直接对 capture() 返回的张量做预处理19;
  3. 先空跑几次预热。 两个理由:让模型权重先加载进显卡、把底层程序编译好; 以及给摄像头硬件一点时间——有些摄像头刚启动时吐的是空白帧20

麦克风同理,而且它的参数更细。 书给的那组配置正好能解释第 06 章那张 43×232 的时频谱21:

fftSize: 1024 每 1024 个采样点算一帧
sampleRateHz: 44100 采样率;能测到的最高频率是它的一半,约 22kHz
columnTruncateLength: 232 只留低频那一段 —— 人耳主要听 0~5kHz
232 是这么来的:(5kHz / 22kHz) × 1024 ≈ 232
numFramesPerSpectrogram: 43 每帧时长 = 1024 / 44100 ≈ 0.023 秒
43 × 0.023 ≈ 1 秒

图说:第 06 章那张「43 × 232 的图」是这么来的。
麦克风同样要预热 —— 书说前 200 毫秒的数据经常是接近零或者无穷大的垃圾。

7. 浏览器端独有的一笔账:显存不会自动回收

这一节是这本书相对母本最不可替代的内容之一,而且它会让你的应用当场崩溃。

先看现象:一个每帧推断的循环,内存一直涨

while (isPredicting) {
const img = await webcam.capture();
const y = model.predict(img);
// …… 用 y ……
}

这段代码会一直漏,直到浏览器报「WebGL 内存不足」。

为什么

因为张量不住在 JavaScript 的堆里。 在浏览器里它们住在 WebGL 的纹理里, 在 Node 里住在系统内存或显存里。JavaScript 的垃圾回收管不着它们22:

TensorFlow.js 不会自动回收用户创建的张量,因为 JavaScript 不支持对象释放。22

注意「用户创建的」这几个字。 库函数内部造的中间张量它自己会管—— predict()fit() 这类调用里面的临时张量不用你操心23要你管的是你自己拿到手的那些。

两个工具

// 办法一:手动回收
const y = model.predict(x);
y.print();
tf.dispose(y);
console.log(tf.memory().numTensors); // ← 盯着这个数,它不该一直涨

// 办法二:整段包起来
tf.tidy(() => {
const y = model.predict(x);
y.print();
}); // ← 这个函数里造的张量,出了 tidy 全部自动回收

tf.tidy 有两条必须记住的规矩24:

  1. 返回值不会被回收。 你写一个自定义运算、要把结果返回出去时, tidy 会自动放过那个返回的张量;
  2. 传给 tidy 的函数不能是异步的。 异步代码只能老老实实用 tf.dispose(), 自己记着哪些要回收。书建议为此写单元测试——这一条在第 20 章会兑现。

怎么知道自己漏没漏? tf.memory().numTensors 直接给你当前活着的张量数24跑一个循环,盯着这个数:它该稳定,不该一路涨。

8. 作者的判断与证据

书里给了证据的:

  • fit() 装不下 25 万张图。 150GB 这个数是书当场算的:250000 × 224 × 224 × 3 × 425
  • 先跳过再转换真的省算力。 有计数器实验,结果是 0 对 610
  • 惰性是真的惰性。 掷骰子那个例子里,取一个样例之后计数器是 17
  • 流式训练能训出东西。 纸牌游戏 25 万样例、约 75% 准确率(瞎猜是 50%)16
  • 张量不自动回收。 书给了会漏的代码和 numTensors 的读数22

属于作者判断、书里没给证据的:

  • 「如果机器学习是这个时代的太空竞赛,那么数据显然就是火箭的燃料。」 这是引用别人的一个比喻26
  • 「用 tf.data 是一种非常好的软件工程实践。」 这是工程经验判断2
  • 「Kaggle 上三分之二的数据集是 CSV 格式。」 这是书给的一个 2019 年 1 月的统计, 用来支持「为什么要专门支持 CSV」27

判断(我们的,不是书里的): 这一章里真正会绊倒人的不是 API,是第 4 节那个 「shuffle 只在窗口内打乱」。它看起来是个性能细节,实际上是一颗定时炸弹—— 第 09 章那个「加州房价按经度排好序」的事故,根子就在这里: 窗口开小了,打乱等于没打乱,而你从损失曲线上完全看不出来。 如果错,会错在: 如果你的数据源本来就是随机顺序的(比如实时生成的), 那么窗口大小无关紧要,这条警告对你不成立。

9. 边界与局限

  • 流式标准化只是个估计。 无限流上算不出真均值,书自己承认了,还提醒了溢出风险1213
  • shuffle 的窗口该开多大,这一章没答。 它是第 09 章的主题。
  • 麦克风流基本不能用来训练。 书说得很直接:很难把音频流和标签打包在一起, 它主要是给推断用的28
  • 摄像头流不能直接喂给 model.fit() 理由和不能用 forEach 一样18
  • 本章的所有数据都还是「干净」的。 有没有离群值、有没有缺失、有没有偏斜, 一个字没提——那是第 09 章。
  • tf.tidy 管不了异步。 而深度学习里几乎所有 I/O 都是异步的, 所以真实项目里你还是得手动 dispose

10. 可带走的

  1. 数据大到读不进来时,把它变成一条惰性的流。 声明时一个字节都不读,取到哪条才拿哪条;
  2. 三种造法: 现成数组、CSV 网址(一行代码)、生成器函数(能造无限多样例);
  3. 无限的数据集必须先 take(n) 直接 toArray() 会一直跑到崩溃;
  4. toArray()forEachAsync() 都是异步的,忘了 await 会让你误以为数据集是空的;
  5. 链式调用是在写说明书,不是在搬数据。 所以skipmap—— 反过来会白白转换 6 个用不上的元素(书里有可验算的计数器实验);
  6. shuffle 只在窗口内打乱。 窗口开小了,远处的元素永远换不到前面来。 记住这一条,第 09 章要用;
  7. 切训练集和测试集用同一个随机种子,两次 shuffle 的结果才一样,take/skip 才不会重叠;
  8. 流上算不出真均值。 一千万个 0 里混一个 10 亿,真均值是 100,前一百万个样例给你 0。 只能一边走一边估;
  9. 训练换成 fitDataset,而且必须给 batchesPerEpoch——流不会自己说「我到头了」;
  10. 张量住在显卡里,JavaScript 的垃圾回收管不着。 每帧推断的循环一定要 tf.dispose()tf.tidy();盯着 tf.memory().numTensors,它该稳定不该涨

11. 原文地图

主题原书章原文位置
两个开场问题(邮件库、图库)第 6 章text/18-ch06.txt:29(搜「垃圾邮件过滤器」)
数据是火箭燃料第 6 章text/18-ch06.txt:13(搜「火箭的燃料」)
「下载-转换-批次化」模式第 6 章text/18-ch06.txt:23(搜「下载-转换-批次化」)
数据集对象 = 惰性流、类似 Node Stream第 6 章text/18-ch06.txt:40(搜「按需下载、读取或执行函数」)
「元素」的定义第 6 章text/18-ch06.txt:42(搜「元素和样例或数据点是同义词」)
V8 内存上限 1.4GB;fitDataset 不占满显存第 6 章text/18-ch06.txt:78(搜「1.4GB」)
三种创建方式第 6 章text/18-ch06.txt:46(搜「三种方式」) · text/18-ch06.txt:60(搜「tf.data.array」)
掷骰子生成器、无限数据集、不取不算第 6 章text/18-ch06.txt:131(搜「rollTwoDiceGeneratorFn」) · text/18-ch06.txt:143(搜「直至服务器或浏览器崩溃」) · text/18-ch06.txt:147(搜「生成器函数只被调用了一次」)
两种读法都是异步的、忘 await 的坑第 6 章text/18-ch06.txt:195(搜「toArray」) · text/18-ch06.txt:199(搜「常见的bug」)
链式方法表、shuffle 窗口第 6 章text/18-ch06.txt:227(搜「一个小程序」) · text/18-ch06.txt:283(搜「bufferSize」)
同种子划分训练/测试第 6 章text/18-ch06.txt:292(搜「相同的随机种子」)
先 skip 后 map 的计数器实验第 6 章text/18-ch06.txt:303(搜「这是对算力的浪费」) · text/18-ch06.txt:331(搜「count is 0」)
无限流算不出均值(1e9 例子)、闭包累计第 6 章text/18-ch06.txt:334(搜「1e9」) · text/18-ch06.txt:341(搜「samplesSoFar」) · text/18-ch06.txt:355(搜「数值溢出」)
fitDataset 的元素格式与三个配置第 6 章text/18-ch06.txt:373(搜「fitDataset」) · text/18-ch06.txt:495(搜「batchesPerEpoch」) · text/18-ch06.txt:503(搜「validationBatches」)
150GB 那笔账第 6 章text/18-ch06.txt:397(搜「150GB」) · text/18-ch06.txt:399(搜「250 000 × 224」)
纸牌游戏:规则、25 万样例、75%第 6 章text/18-ch06.txt:385(搜「同数值牌」) · text/18-ch06.txt:393(搜「75%的准确率」)
CSV:列名查询、按行取、columnConfigs第 6 章text/18-ch06.txt:548(搜「columnNames()方法」) · text/18-ch06.txt:559(搜「打印出CSV文件中选中的一行」) · text/18-ch06.txt:573(搜「crim: 0.3237」)
Kaggle 三分之二是 CSV第 6 章text/18-ch06.txt:515(搜「13 971个公有数据集」)
摄像头流:两处不同、三条规矩第 6 章text/18-ch06.txt:679(搜「取决于数据获取的时机」) · text/18-ch06.txt:681(搜「而不是使用tf.data提供的延后执行」) · text/18-ch06.txt:698(搜「不能使用forEach」) · text/18-ch06.txt:721(搜「预热」)
麦克风流的参数与 43×232 的来历第 6 章text/18-ch06.txt:764(搜「快速傅里叶变换」) · text/18-ch06.txt:782(搜「232」) · text/18-ch06.txt:790(搜「约为1秒」) · text/18-ch06.txt:804(搜「预热」)
张量不自动回收、tf.disposetf.tidynumTensors附录 Btext/27-apx-b-b-tensorflow-js.txt:490(搜「不支持对象释放」) · text/27-apx-b-b-tensorflow-js.txt:502(搜「numTensors」) · text/27-apx-b-b-tensorflow-js.txt:595(搜「不能是异步的」)

Footnotes

  1. 出处:「第 6 章 处理数据」第 29 段(text/18-ch06.txt:29,搜「垃圾邮件过滤器」)。

  2. 出处:「第 6 章」第 78 段(text/18-ch06.txt:78,搜「一团乱麻」)。同段给出了 V8 引擎在 64 位系统上 1.4GB 的内存上限。 2

  3. 出处:「第 6 章」第 40 段(text/18-ch06.txt:40,搜「按需下载、读取或执行函数」)。 2

  4. 出处:「第 6 章」脚注 3,第 42 段(text/18-ch06.txt:42,搜「元素和样例或数据点是同义词」)。

  5. 出处:「第 6 章」表 6-1,第 46~72 段(text/18-ch06.txt:46,搜「三种方式」)。

  6. 出处:「第 6 章」第 116 段(text/18-ch06.txt:116,搜「不必记住各列的排序」)。Node 环境里还能用 file:// 前缀读本地文件(text/18-ch06.txt:113,搜「file://」)。

  7. 出处:「第 6 章」代码清单 6-3 与第 143~147 段(text/18-ch06.txt:143,搜「直至服务器或浏览器崩溃」;text/18-ch06.txt:147,搜「生成器函数只被调用了一次」)。 2

  8. 出处:「第 6 章」第 195~201 段(text/18-ch06.txt:195,搜「toArray」;text/18-ch06.txt:199,搜「常见的bug」;text/18-ch06.txt:201,搜「高效地利用等待数据生成的时间」)。 2

  9. 出处:「第 6 章」表 6-3,第 229~285 段(text/18-ch06.txt:227,搜「一个小程序」;text/18-ch06.txt:283,搜「bufferSize」)。

  10. 出处:「第 6 章」代码清单 6-5 与第 303~333 段(text/18-ch06.txt:303,搜「这是对算力的浪费」;text/18-ch06.txt:331,搜「count is 0」)。 2

  11. 出处:「第 6 章」代码清单 6-4 与第 289~301 段(text/18-ch06.txt:292,搜「相同的随机种子」;text/18-ch06.txt:301,搜「两个数据集中的样例没有重叠」)。

  12. 出处:「第 6 章」第 334 段(text/18-ch06.txt:334,搜「1e9」)。 2

  13. 出处:「第 6 章」代码清单 6-6 与第 340、355 段(text/18-ch06.txt:341,搜「samplesSoFar」;text/18-ch06.txt:355,搜「数值溢出」)。 2 3

  14. 出处:「第 6 章」第 373 段(text/18-ch06.txt:373,搜「fitDataset」)。

  15. 出处:「第 6 章」第 495~503 段(text/18-ch06.txt:495,搜「batchesPerEpoch」;text/18-ch06.txt:503,搜「validationBatches」)。原文点明:不配置 validationBatches 时,面对无限数据集「程序因此会卡住」。

  16. 出处:「第 6 章」第 383~393 段(text/18-ch06.txt:385,搜「同数值牌」;text/18-ch06.txt:393,搜「75%的准确率」)。原文说明双方胜率相同,所以瞎猜只有约一半正确率。 2

  17. 出处:「第 6 章」第 679 段(text/18-ch06.txt:679,搜「取决于数据获取的时机」)。

  18. 出处:「第 6 章」第 698 段(text/18-ch06.txt:698,搜「不能使用forEach」)。原文:那样「张量创建的频率会超过设备的帧率,从而造成图像数据的重复和算力的浪费」;同段还说明摄像头数据集不能直接传给 model.fit() 2

  19. 出处:「第 6 章」第 681~695 段(text/18-ch06.txt:681,搜「而不是使用tf.data提供的延后执行」)。

  20. 出处:「第 6 章」第 721 段(text/18-ch06.txt:721,搜「预热」)。

  21. 出处:「第 6 章」第 764~804 段(text/18-ch06.txt:782,搜「232」;text/18-ch06.txt:790,搜「约为1秒」;text/18-ch06.txt:804,搜「预热」)。注意: 原文在算每帧时长时写成「44kHz × 1024」,按上下文应是 1024 / 44100,结果 0.023 秒是对的。

  22. 出处:「附录 B」第 490 段(text/27-apx-b-b-tensorflow-js.txt:490,搜「不支持对象释放」)。 2 3

  23. 出处:「附录 B」脚注 5,第 492 段(text/27-apx-b-b-tensorflow-js.txt:492,搜「由TensorFlow.js库自动管理」)。原文点名 tf.confusionMatrix()tf.Model.predict()tf.Model.fit()

  24. 出处:「附录 B」第 569 与 595 段(text/27-apx-b-b-tensorflow-js.txt:528,搜「函数返回的张量除外」;text/27-apx-b-b-tensorflow-js.txt:595,搜「不能是异步的」)。原文建议「通过编写单元测试来确保没有内存泄漏」。 2

  25. 出处:「第 6 章」第 397 段与脚注 8,第 397 段(text/18-ch06.txt:397,搜「150GB」;text/18-ch06.txt:399,搜「250 000 × 224」)。

  26. 出处:「第 6 章」第 13 段与脚注 2(text/18-ch06.txt:13,搜「火箭的燃料」)。这个比喻出自 Edd Dumbill 的文章 "Big Data Is Rocket Fuel"。

  27. 出处:「第 6 章」脚注 9,第 515 段(text/18-ch06.txt:515,搜「13 971个公有数据集」)。

  28. 出处:「第 6 章」第 745 段(text/18-ch06.txt:745,搜「并不适用于模型的训练」)。