2. TensorFlow.js

TensorFlow.js 是一个用于机器学习的开源 JavaScript 库。 它由 Google 开发,是 Python 中 TensorFlow 的配套库。

此工具使您能够构建可在浏览器或 Node.js 中运行的机器学习应用程序。

这样,用户无需安装任何软件或驱动程序。 只需打开网页即可与程序进行交互。

2.1 TensorFlow.js 基础
TensorFlow.js 由 WebGL 提供支持,并提供用于定义模型的高级层 API 和用于线性代数的低级 API(以前是 deeplearn.js)。


它还支持导入从 TensorFlow 和 Keras 保存的模型。

TensorFlow 的核心是张量。 张量是一种数据单位,是一组形成一个或多个维度的数组的值。 它类似于多维数组。

2.1.1 创建张量

例如,您可以想象以下示例二维数组。

const data = [
     [0.456, 0.378, 0.215],
     [0.876, 0.938, 0.276],
     [0.629, 0.287, 0.518]
];

将其转换为张量以便与 TensorFlow.js 一起使用的方法是使用内置方法 tf.tensor 包装它。

const data = [
     [0.456, 0.378, 0.215],
     [0.876, 0.938, 0.276],
     [0.629, 0.287, 0.518]
];
const dataTensor = tf.tensor(data);

现在,变量 dataTensor 可以与其他 TensorFlow 方法一起使用来训练模型、生成预测等。

一些额外的属性可以在 tf.tensor 中访问,例如 rank、shape 和 dtype。
rank:表示张量包含多少维

shape:定义数据每个维度的大小

dtype:定义张量的数据类型

const tensor = tf.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
console.log("shape: ", tensor.shape); // [3,3]
console.log("rank: ", tensor.rank); // 2
console.log("dtype: ", tensor.dtype); // float32

在所示的示例张量中,形状返回 [3, 3],因为我们可以看到 3 个包含 3 个值的数组。

当我们使用二维数组时,rank 属性会打印 2。 如果我们向数组中添加了另一个维度,则排名将为 3。

最后,dtype 是 float32,因为这是默认数据类型。

还可以使用其他数据类型(如 bool、int32、complex64 和 string dtypes)创建张量。 为此,我们需要将形状作为第二个参数传递,将 dtype 作为第三个参数传递给 tf.tensor。

const tensor = tf.tensor([[1, 2], [4, 5]], [2,2], "int32");
console.log("shape: ", tensor.shape); // [2,2]
console.log("rank: ", tensor.rank); // 2
console.log("dtype: ", tensor.dtype); // int32

在到目前为止显示的示例代码中,我们使用 tf.tensor 创建张量; 但是,可以使用更多方法来创建具有不同维度的它们。

根据您正在处理的数据,您可以使用从 tf.tensor1d 到 tf.tensor6d 的方法来创建最多六维的张量。

如果你要转换的数据是一个六层的多维数组,你可以同时使用 tf.tensor 和 tf.tensor6d; 但是,使用 tf.tensor6d 会使代码更具可读性,因为您可以自动知道维度的数量。

const tensor = tf.tensor6d([
  [
    [
      [
        [[1], [2]],
        [[3], [4]]
      ],
      [
        [[5], [6]],
        [[7], [8]]
      ]
    ]
  ]
]);
// Is the same thing as
const sameTensor = tf.tensor([
  [
    [
      [
        [[1], [2]],
        [[3], [4]]
      ],
      [
        [[5], [6]],
        [[7], [8]]
      ]
    ]
  ]
]);

创建张量时,您还可以传入一个平面数组并指示您希望张量具有的形状。

const tensor = tf.tensor2d([
  [1, 2, 3],
  [4, 5, 6]
]);
// is the same thing as
const sameTensor = tf.tensor([1, 2, 3, 4, 5, 6], [2, 3]);

一旦张量被实例化,就可以使用 reshape 方法改变它的形状。

2.1.2 访问张量中的数据

创建张量后,您可以使用 tf.array() 或 tf.data() 获取其值。

tf.array() 返回值的多维数组,而 tf.data() 返回扁平化的数据。

const tensor = tf.tensor2d([[1, 2, 3], [4, 5, 6]]);
const array = tensor.array().then(values => console.log("array: ": values));
// array: [ [1, 2, 3], [4, 5, 6] ]
const data = tensor.data().then(values => console.log("data: ", values));
// data: Float32Array [1, 2, 3, 4, 5, 6];

正如您在前面的示例中看到的,这两个方法返回一个承诺。

在 JavaScript 中,promise 是在调用 promise 时尚未创建的值的代理。 Promise 表示尚未完成的操作,因此它们与异步操作一起使用,以在操作完成后的某个时间点提供值。

但是,还使用 arraySync() 和 dataSync() 提供了同步版本。

const tensor = tf.tensor2d([[1, 2, 3], [4, 5, 6]]);
const values = tensor.arraySync();
console.log("values: ": values); // values: [ [1, 2, 3], [4, 5, 6] ]
const data = tensor.dataSync()
console.log("data: ", values); // data: Float32Array [1, 2, 3, 4, 5, 6];

不建议在生产应用程序中使用它们,因为它们会导致性能问题。

2.1.3 张量运算

在上一节中,我们了解到张量是一种数据结构,允许我们以 TensorFlow.js 可以使用的方式存储数据。 我们看到了如何创造它们、塑造它们以及获取它们的价值。

现在,让我们研究一些允许我们操纵它们的不同操作。

这些操作可以组织成类别。 其中一些允许您对张量进行算术运算,例如,将多个张量相加,其他操作专注于执行逻辑运算,例如评估张量是否大于另一个,而其他操作则提供了一种进行基本数学运算的方法,例如计算 张量中所有元素的平方。

完整的操作列表可在 https://js.tensorflow.org/api/latest/#Operations 获得。

以下是如何使用这些操作的示例。

const tensorA = tf.tensor([1, 2, 3, 4]);
const tensorB = tf.tensor([5, 6, 7, 8]);
const tensor = tf.add(tensorA, tensorB); // [6, 8, 10, 12]
// or
// const tensor = tensorA.add(tensorB);

在这个例子中,我们将两个张量相加。 如果您查看 tensorA 的第一个值,即 1,以及 tensorB 的第一个值,即 5,那么 1 + 5 的结果是数字 6,即我们最终张量的第一个值。

为了能够使用这种操作,您的张量必须具有相同的形状,但不一定具有相同的等级。

如果您还记得前几页,形状是张量每个维度中值的数量,而秩是维度数量。

让我们用另一个例子来说明这一点。

const tensorA = tf.tensor2d([[1, 2, 3, 4]]);
const tensorB = tf.tensor([5, 6, 7, 8]);
const tensor = tf.add(tensorA, tensorB); // [[6, 8, 10, 12],]

在这种情况下,tensorA 现在是一个 2D 张量,但 tensorB 仍然是一维的。

将两者相加的结果现在是一个张量,其值与以前相同,但维数不同。

但是,如果我们尝试添加多个不同形状的张量,则会导致错误。

const tensorA = tf.tensor([1, 2, 3, 4]);
const tensorB = tf.tensor([5, 6, 7]);
const tensor = tf.add(tensorA, tensorB);
// Error: Operands could not be broadcast together with shapes 3 and 4.

这个错误告诉我们的是,这个操作数不能与这两个张量一起使用,因为其中一个有四个元素,另一个只有三个。

张量是不可变的,因此这些操作不会改变原始张量,而是始终返回一个新的 tf.Tensor。

2.1.4 内存

最后,在使用张量时,您需要使用 dispose() 或 tf.dispose() 显式清除内存。

const tensor = tf.tensor([1, 2, 3, 4]);
tensor.dispose();
// or
tf.dispose(tensor);

另一种管理内存的方法是在链接操作时使用 tf.tidy() 。

由于张量是不可变的,因此每次操作的结果都是一个新的张量。 为了避免对您生成的所有张量调用 dispose,使用 tf.tidy() 允许您只保留所有操作生成的最后一个并处理所有其他张量。

const tensorA = tf.tensor([1, 2, 3, 4]);
const tensor = tf.tidy(() => {
  return tensorA.square().neg();
});
console.log(tensor.dataSync()); // [-1, -4, -9, -16]

在这个例子中,square() 的结果将被处理,而 neg() 的结果不会因为它返回函数的值。

现在我们已经介绍了 TensorFlow.js 的核心内容以及如何使用张量,让我们看看该库提供的不同功能,以更好地了解可能的情况。

2.2 特点

在本子章节中,我们将探索 TensorFlow.js 当前可用的三个主要功能。这包括使用预先训练的模型;进行迁移学习,这意味着使用自定义输入数据重新训练模型;并在 JavaScript 中完成所有工作,即创建模型、训练模型和运行预测,所有这些都在浏览器中完成。

我们将涵盖从最简单到最复杂的这些功能。

2.2.1 使用预训练模型

在本书的第一章中,我们将术语“模型”定义为一个数学函数,它可以采用新的参数,根据训练过的数据进行预测。

如果这个定义对你来说仍然有点混乱,希望在谈论第一个特性时把它放在上下文中会让它更清楚一些。

在机器学习中,为了能够预测结果,我们需要一个模型。但是,没有必要自己构建模型。使用所谓的“预训练模型”完全没问题。

术语“预训练”意味着该模型已经用某种类型的输入数据进行了训练,并且是为特定目的而开发的。

例如,您可以找到一些专注于对象检测和识别的开源预训练模型。这些模型已经接受了数百万个对象的图像,已经完成了所有的训练过程,现在在预测新实体时应该具有令人满意的准确度。

创建这些模型的公司或机构将它们开源,因此开发人员可以在他们的应用程序中使用它们,并有机会更快地构建机器学习项目。

可以想象,收集数据、格式化、标记、试验不同算法和参数的过程可能需要大量时间,因此能够使用预训练模型替代这项工作可以节省大量时间专注于构建应用程序。

当前可用于 TensorFlow.js 的预训练模型包括身体分割、姿势估计、对象检测、图像分类、语音命令识别和情感分析。

在您的应用程序中使用预先训练的模型相对容易。

在以下代码示例中,我们将使用 mobilenet 对象检测模型来预测新图像中的实体。

const img = document.getElementById("img");
const model = await mobilenet.load();
const predictions = await model.classify(img);
return predictions;

在实际应用中,此代码需要事先需要 TensorFlow.js 库和 mobilenet 预训练模型,但在我们深入构建实际项目时,将在接下来的几章中展示更完整的代码示例。

前面的示例首先获取应包含我们想要预测的图像的 HTML 元素。 下一步是异步加载 mobilenet 模型。

模型的大小可能相当大,有时有几兆字节,因此需要使用 async/await 加载它们,以确保在您运行预测时完全完成此操作。

模型准备就绪后,您可以调用该模型的分类()方法,在该方法中传递您的 HTML 元素,该方法将返回一个预测数组。

在您将使用猫图像的示例中,预测的输出看起来与此类似。




使用classify() 的结果始终是一个包含三个对象的数组,其中包含两个键:className 和probability。

className 是一个包含标签或类的字符串,模型根据之前训练过的数据对新输入进行了分类。

概率是一个介于 0 和 1 之间的浮点值,表示输入数据属于 className 的可能性,0 表示不太可能,1 表示非常可能。

它们按降序排列,因此数组中的第一个对象是最有可能为真的预测。

在之前的输出中,模型以 70% 的可能性预测图像包含“老虎猫”。

其余预测的概率值大幅下降,其中包含“虎斑猫”的概率为 21%,包含“领结”的概率约为 0.02%。

通常,您会关注预测中返回的第一个值,因为它的概率最高;然而,70%实际上并没有那么高。

在机器学习中,您的目标是在使用预测时获得尽可能高的概率。在这种情况下,我们只预测了图像中猫的存在,但在实际应用中,您可以想象 30% 的预测错误输出的可能性是不可接受的。

为了改善这一点,一般来说,我们会做所谓的“超参数调整”并重新训练模型。

超参数调整是调整和优化生成模型时使用的参数的过程。它可能是在神经网络中添加层、更改批量大小等,并查看这些更改对模型性能和准确性的影响。

但是,在使用预训练模型时,您将无法执行此操作,因为您唯一可以访问的是输出模型,而不是为创建它而编写的代码。

这是使用预训练模型的限制之一。

使用这些模型时,您无法控制它们的创建方式和修改方式。您通常无法访问训练过程中使用的数据集,因此您无法确定它是否满足您的应用程序的要求。

此外,您还冒着继承公司或机构偏见的风险。

如果您的应用程序涉及实施面部识别,并且您决定使用开源预训练模型,则无法确定该模型是在不同的人数据集上训练的。因此,您可能会在不知不觉中通过使用某些偏见来支持它们。

过去曾出现过面部识别模型仅在白人身上表现良好的问题,而留下了大量肤色较深的用户。

尽管已经做了一些工作来解决这个问题,但我们经常听到机器学习模型做出有偏见的预测,因为用于训练它们的数据不够多样化。

如果您决定在生产应用程序中使用预先训练的模型,我认为事先做一些研究很重要。

2.2.2 迁移学习

TensorFlow.js 中可用的第二个功能称为“迁移学习”。

迁移学习是重用为任务开发的模型的能力,作为第二个任务的模型的起点。

如果您想象一个对象识别模型已经在您无法访问的数据集上进行了预训练,那么该模型的核心功能是识别图像中的实体。使用迁移学习,您可以利用此模型创建一个新模型,该模型的功能相同,但使用您的自定义输入数据进行训练。

迁移学习是一种生成半定制模型的方法。您仍然无法修改模型本身,但您可以将自己的数据提供给它,这可以提高预测的准确性。

如果我们重用上一节中使用预训练模型检测图片中是否存在猫的示例,我们可以看到预测返回时带有“老虎猫”标签。这意味着模型是用标记为这样的图像进行训练的,但是如果我们想要检测非常不同的东西,比如金荆树(澳大利亚花)怎么办?

第一步是搜索模型可以预测的类别列表,看看它是否包含这些花。如果是,则表示该模型可以直接使用,就像上一节所示。

但是,如果它没有使用 Golden Wattles 的图像进行训练,那么在我们使用迁移学习生成新模型之前,它将无法检测到它们。

为此,部分代码类似于上一节中显示的示例,因为我们仍然需要从预训练的模型开始,但我们引入了一些新逻辑。

我们需要首先将 K 近邻分类器与 TensorFlow.js 和 mobilenet 预训练模型一起导入到我们的应用程序中。




这样做使我们可以访问 knnClassifier 对象。

要实例化它,我们需要调用 create 方法。

const classifier = knnClassifier.create();

该分类器将用于使我们能够根据自定义输入数据进行预测,而不仅仅是使用预训练模型。

此过程中的主要步骤涉及对模型进行所谓的推理,这意味着将 mobilenet 模型应用于新数据,将这些示例添加到分类器,并预测类别。

const img = await webcam.capture();
const activation = model.infer(img, 'conv_preds');
classifier.addExample(activation, classId);

前面的代码示例不完整,但当我们专注于在应用程序中实现迁移学习时,我们将在接下来的章节中更深入地介绍它。

这里最重要的是要了解我们将网络摄像头馈送中的图像保存在一个变量中,将其用作模型上的新数据,并将其作为带有类(标签)的示例添加到分类器中,因此最终结果是 一种模型,它不仅能够识别与 mobilenet 模型初始训练过程中使用的数据相似的数据,还能够识别我们的新样本。

向分类器提供单个新图像和示例还不足以使其能够准确识别我们的新输入数据; 因此,此步骤必须重复多次。

一旦您认为您的分类器已准备就绪,您就可以像这样预测输入。

const img = await webcam.capture();
const activation = model.infer(img, 'conv_preds');
const result = await classifier.predictClass(activation);
const classes = ["A", "B", "C"];
const prediction = classes[result.label];

第一步是相同的,但我们没有将示例添加到分类器中,而是使用 predictClass 方法返回它认为新输入的结果。

我们将在下一章更深入地讨论迁移学习。

2.2.3 创建、训练和预测
最后,第三个功能允许您自己创建模型、运行训练过程并使用它,所有这些都在 JavaScript 中。

此功能比前两个功能更复杂,但将在第 5 章中更深入地介绍,当我们使用我们自己创建的模型构建应用程序时。

重要的是要知道自己创建模型需要反复试验的方法。

没有单一的方法可以解决问题,如果您决定沿着这条路走下去,您将需要对不同的算法、参数等进行大量试验。

最常用的模型类型是可以使用层列表创建的顺序模型。

此类模型的示例可能如下所示。

const model = tf.sequential();
model.add(tf.layers.conv2d({
      inputShape: [28, 28, 1],
      kernelSize: 5,
      filters: 8,
      strides: 1,
      activation: 'relu',
      kernelInitializer: 'VarianceScaling'
}));
model.add(tf.layers.maxPooling2d({
      poolSize: [2, 2],
      strides: [2, 2]
}));
model.add(tf.layers.conv2d({
      kernelSize: 5,
      filters: 16,
      strides: 1,
      activation: 'relu',
      kernelInitializer: 'VarianceScaling'
}));

我们首先使用 tf.sequential 实例化它,并向其添加多个不同的层。

这一步有点随意,因为选择层的类型和数量,以及传递给层的参数,与其说是科学,不如说是一门艺术。

您的模型在您第一次编写时可能并不完美,并且需要多次更改才能最终得到性能最高的结果。

要记住的一件重要事情是在模型的第一层提供 inputShape 参数,以指示模型将要训练的数据的形状。 后续层不需要它。

创建模型后,下一步是用数据对其进行训练。 这一步是使用 fit 方法完成的。

await model.fit(data, label, options);

通常,在调用此方法之前,您需要将数据分批以一点一点地训练模型。整个数据集通常太大而无法一次使用,因此将其分成批次很重要。

传递给函数的 options 参数是一个包含有关训练过程信息的对象。您可以指定 epoch 数,即整个数据集通过神经网络的时间,以及批次大小,表示单个批次中存在的训练示例数。

由于数据集在 fit 方法中被分批传递,因此我们还需要考虑使用完整数据集训练模型所需的迭代次数。

例如,如果我们的数据集包含 1000 个示例,并且我们的批次大小为一次 100 个示例,则需要 10 次迭代才能完成 1 个 epoch。

因此,我们需要循环调用我们的 fit 方法 10 次,每次更新批处理数据。

一旦模型经过完全训练,就可以使用 predict 方法进行预测。

const prediction = model.predict(data);

关于此功能还有更多内容要介绍,但我们将在接下来的几章中通过实际示例进一步研究它。

你可能感兴趣的:(2. TensorFlow.js)