首页 > 其他分享 >lrn-tfjs-merge-1

lrn-tfjs-merge-1

时间:2024-02-08 15:45:07浏览次数:23  
标签:const tensor JavaScript 张量 lrn merge 图像 tf tfjs

TensorFlow.js 学习手册(二)

原文:Learning TensorFlow.js

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:引入张量

“哇!”

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

我们已经多次提到张量这个词,它是 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的实现存在错误,可能会有问题

标签:const,tensor,JavaScript,张量,lrn,merge,图像,tf,tfjs
From: https://www.cnblogs.com/apachecn/p/18011861

相关文章

  • lrn-tf-merge-2
    TensorFlow学习手册(三)原文:LearningTensorFlow译者:飞龙协议:CCBY-NC-SA4.0第六章:文本II:词向量、高级RNN和嵌入可视化在本章中,我们深入探讨了在第五章中讨论的与文本序列处理相关的重要主题。我们首先展示了如何使用一种称为word2vec的无监督方法训练词向量,以及如何使......
  • lrn-tf-merge-3
    TensorFlow学习手册(四)原文:LearningTensorFlow译者:飞龙协议:CCBY-NC-SA4.0第八章:队列、线程和读取数据在本章中,我们介绍了在TensorFlow中使用队列和线程的方法,主要目的是简化读取输入数据的过程。我们展示了如何编写和读取TFRecords,这是高效的TensorFlow文件格式。......
  • 聊聊ClickHouse MergeTree引擎的固定/自适应索引粒度
    ​ 前言我们在刚开始学习ClickHouse的MergeTree引擎时,就会发现建表语句的末尾总会有SETTINGSindex_granularity=8192这句话(其实不写也可以),表示索引粒度为8192。在每个datapart中,索引粒度参数的含义有二:每隔index_granularity行对主键组的数据进行采样,形成稀疏索引,并存储......
  • ABC306F Merge Sets
    原题链接分析观察要求的式子:\(\sum_{1\leqi\ltj\leqN}f(S_i,S_j)\),发现可以拆成每一个集合\(S_i\)的贡献的和。那么我们考虑每一个集合\(S_i\)的贡献。显然,对于每一个\(S_i\),其贡献就是\(\sum_{i\ltj\leqN}f(S_i,S_j)\),也就是它与其后每一个集合\(S_j\)的......
  • SQL Server MERGE(合并)语句
    来源 https://www.cnblogs.com/yigegaozhongsheng/p/11941734.html如何使用SQLServerMERGE语句基于与另一个表匹配的值来更新表中的数据。  SQLServer MERGE语句 假设有两个表,分别称为源表和目标表,并且需要根据与源表匹配的值来更新目标表。有以下三种情况: 源表......
  • Git必知必会基础(12):远程冲突(conflicts)解决--merge
     演示场景虽然每次合并代码前会先把分支更新到最新,但是在你pull后到push前这段时间,可能其它小伙伴又push了,那么你的分支就不是最新的了在push的时候就会失败,比如遇到这种提示信息:Togitee.com:qzcsbj/pytest_apiautotest.git![rejected]master->master(fetchfirst)error:......
  • 使用mergekit 合并大型语言模型
    模型合并是近年来兴起的一种新技术。它允许将多个模型合并成一个模型。这样做不仅可以保持质量,还可以获得额外的好处。假设我们有几个模型:一个擅长解决数学问题,另一个擅长编写代码。在两种模型之间切换是一个很麻烦的问题,但是我们可以将它们组合起来,利用两者的优点。而且这种组......
  • Git必知必会基础(11):merge和rebase的区别
     本系列汇总,请查看这里:https://www.cnblogs.com/uncleyong/p/10854115.htmlmerge和rebase使用回顾上两篇我们分别演示了merge和rebase的使用,分别详见:https://www.cnblogs.com/uncleyong/p/17967432https://www.cnblogs.com/uncleyong/p/17978213下面我们来总结下二者的差异......
  • Git必知必会基础(09):本地冲突(conflicts)解决--merge
     本系列汇总,请查看这里:https://www.cnblogs.com/uncleyong/p/10854115.html准备数据 远程数据远程commitid 克隆到本地 创建dev_1分支 修改qzcsbj.txt内容,然后提交到本地仓库,最后推送到远程仓库 切换到master,创建dev_2分支 修改qzcsbj.txt内容,然后提......
  • Git必知必会基础(10):远程冲突(conflicts)解决--merge
     本系列汇总,请查看这里:https://www.cnblogs.com/uncleyong/p/10854115.html数据准备重新克隆 日志 远程分支qzcsbj.txt内容 commitid 其他人提交模拟其他人对master做了提交:直接gitee上修改文件并提交 新的commitid 本地提交本地分支修改qzcsbj.t......