TensorFlow.js - 根据 2D 数据进行预测

在此 Codelab 中,您将训练模型,使之能够根据描述一组汽车的数值数据做出预测。

本练习将演示训练许多不同类型的模型的常见步骤,但将使用小型数据集和简单(浅显)模型。主要目标是帮助您熟悉有关使用 TensorFlow.js 训练模型的基本术语、概念和语法,让您为进一步探索和学习打下良好的基础。

由于我们训练模型来预测连续数字,因而此任务有时称为回归任务。我们将通过向模型展示输入的多个样本和正确的输出来训练模型。这称为监督式学习

您将构建的模型

您将创建一个使用 TensorFlow.js 在浏览器中训练模型的网页。如果提供汽车的“马力”,模型将学习预测“每加仑的英里数”(MPG)。

为了实现此目的,您需要:

  • 加载数据,并准备将其用于训练。
  • 定义模型的架构。
  • 训练模型并监控其训练时的性能。
  • 通过进行一些预测来评估经过训练的模型。

学习内容

  • 准备用于机器学习的数据的最佳做法,包括重排和归一化。
  • TensorFlow.js 语法:只有掌握了这些语法,才能使用 tf.layers API 创建模型。
  • 如何使用 tfjs-vis 库监控浏览器内训练。

所需条件

创建 HTML 网页并添加 JavaScript

96914ff65fc3b74c.png 将以下代码复制到名为 index.html 的 HTML 文件中

index.html

<!DOCTYPE html>
<html>
<head>
  <title>TensorFlow.js Tutorial</title>

  <!-- Import TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@2.0.0/dist/tf.min.js"></script>
  <!-- Import tfjs-vis -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>

  <!-- Import the main script file -->
  <script src="script.js"></script>

</head>

<body>
</body>
</html>

为代码创建 JavaScript 文件

  1. 在上述 HTML 文件所在的文件夹中,创建一个名为 script.js 的文件,并将以下代码复制到其中。
console.log('Hello TensorFlow');

开始测试

现在,您已经创建了 HTML 和 JavaScript 文件,接下来可以对其进行测试。在浏览器中打开 index.html 文件,然后打开 Devtools 控制台。

如果一切正常,系统应该在 DevTools 控制台中创建并且提供两个全局变量:

  • tf 是对 TensorFlow.js 库的引用
  • tfvis 是对 tfjs-vis 库的引用

打开浏览器的开发者工具,您应该会在控制台输出中看到一条内容为“Hello TensorFlow”的消息。如果是这样,那么您可以继续执行下一步操作。

我们首先加载要用于训练模型的数据、设置其格式并让数据直观呈现出来。

我们将从为您托管的 JSON 文件中加载“cars”数据集。它包含与每辆指定汽车有关的多个不同特征。在本教程中,我们只希望提取有关马力和每加仑英里数的数据。

96914ff65fc3b74c.png 将以下代码添加到您的

script.js 文件中

/**
 * Get the car data reduced to just the variables we are interested
 * and cleaned of missing data.
 */
async function getData() {
  const carsDataResponse = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');
  const carsData = await carsDataResponse.json();
  const cleaned = carsData.map(car => ({
    mpg: car.Miles_per_Gallon,
    horsepower: car.Horsepower,
  }))
  .filter(car => (car.mpg != null && car.horsepower != null));

  return cleaned;
}

这样还会移除尚未定义每加仑的英里数或马力的所有条目。让我们以散点图的形式绘制这些数据,以查看其呈现样式。

96914ff65fc3b74c.png 将以下代码添加到

script.js 文件的底部。

async function run() {
  // Load and plot the original input data that we are going to train on.
  const data = await getData();
  const values = data.map(d => ({
    x: d.horsepower,
    y: d.mpg,
  }));

  tfvis.render.scatterplot(
    {name: 'Horsepower v MPG'},
    {values},
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );

  // More code will be added below
}

document.addEventListener('DOMContentLoaded', run);

刷新页面时,您应该会在页面左侧看到一个面板,其中显示了数据的散点图。应如下所示。

cf44e823106c758e.png

此面板称为 visor,由 tfjs-vis 提供,可便利地直观显示数据形状。

通常,在处理数据时,建议您设法查看数据并在必要时清理这些数据。在本例,我们必须从 carsData 中移除某些不包含所有必填字段的条目。直观呈现数据可让我们了解模型可以学习的数据是否存在任何结构。

从上图中可以看出,马力与 MPG 之间成反比,也就是说,随着马力越大,汽车耗用一加仑汽油能行使的英里数通常越少。

形成任务的概念

现在,输入数据将如下所示。

...
{
  "mpg":15,
  "horsepower":165,
},
{
  "mpg":18,
  "horsepower":150,
},
{
  "mpg":16,
  "horsepower":150,
},
...

我们的目标是训练一个模型,该模型将获取一个一个数字“马力”,并学习预测一个数字“每加仑的英里数”。请注意,这是一对一的映射,该映射对于下一部分非常重要。

我们将这些样本(马力和 MPG)提供给神经网络,神经网络将通过这些样本学习一个公式(或函数),以预测汽车在指定马力下的 MPG。这种通过从我们提供正确答案的样本来学习的方式称为监督式学习

在本部分中,我们将编写代码来描述模型架构。模型架构其实就是“模型在执行时会运行的函数”,换一种说法就是“我们的模型将用于计算答案的算法”

机器学习模型是指接受输入并生成输出的算法。使用神经网络时,算法是一组神经元层,其中权重(数字)控制着其输出。训练过程会学习这些权重的理想值。

96914ff65fc3b74c.png 将以下函数添加到您的

script.js 文件来定义模型架构。

function createModel() {
  // Create a sequential model
  const model = tf.sequential();

  // Add a single input layer
  model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));

  // Add an output layer
  model.add(tf.layers.dense({units: 1, useBias: true}));

  return model;
}

这是我们在 tensorflow.js 中定义的一个最简单的模型,让我们来详细介绍每一行。

实例化模型

const model = tf.sequential();

这样会实例化 tf.Model 对象。此模型是 sequential,因为其输入直接向下流至其输出。其他类型的模型可以有分支,甚至可以有多个输入和输出,但在许多情况下,模型将是贯序的。贯序模型还更易于使用 API

添加层

model.add(tf.layers.dense({inputShape: [1], units: 1, useBias: true}));

此操作会向我们的网络添加输入层,从而自动连接到包含一个隐藏单元的 dense 层。 dense 是一种层,可将输入与矩阵(称为“权重”)相乘,并向结果添加一个数字(称为“偏差”)。由于这是网络的第一层,因此我们需要定义 inputShapeinputShape[1],因为我们将 1 数字用作输入(某辆指定汽车的马力)。

units 用于设置权重矩阵在层中的大小。将其设置为 1 即表示数据的每个输入特征的权重为 1。

model.add(tf.layers.dense({units: 1}));

上述代码用于创建我们的输出层。我们将 units 设置为 1,因为我们想要输出数字 1

创建实例

96914ff65fc3b74c.png 将以下代码添加到

我们之前定义的 run 函数。

// Create the model
const model = createModel();
tfvis.show.modelSummary({name: 'Model Summary'}, model);

这样将创建模型的实例,并显示网页上各层的摘要。

为了享有 TensorFlow.js 的性能优势,从而让训练机器学习模型切实可行,我们需要将数据转换为张量。我们还会对数据执行许多转换(这是最佳做法),即重排归一化

96914ff65fc3b74c.png 将以下代码添加到您的

script.js 文件中

/**
 * Convert the input data to tensors that we can use for machine
 * learning. We will also do the important best practices of _shuffling_
 * the data and _normalizing_ the data
 * MPG on the y-axis.
 */
function convertToTensor(data) {
  // Wrapping these calculations in a tidy will dispose any
  // intermediate tensors.

  return tf.tidy(() => {
    // Step 1. Shuffle the data
    tf.util.shuffle(data);

    // Step 2. Convert data to Tensor
    const inputs = data.map(d => d.horsepower)
    const labels = data.map(d => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    //Step 3. Normalize the data to the range 0 - 1 using min-max scaling
    const inputMax = inputTensor.max();
    const inputMin = inputTensor.min();
    const labelMax = labelTensor.max();
    const labelMin = labelTensor.min();

    const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
    const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      // Return the min/max bounds so we can use them later.
      inputMax,
      inputMin,
      labelMax,
      labelMin,
    }
  });
}

我们来详细了解一下发生哪些情况。

重排数据

// Step 1. Shuffle the data
tf.util.shuffle(data);

我们会随机排列提供给训练算法的样本的顺序。数据重排很重要,因为在训练期间,数据集通常会被拆分成较小的子集(称为批次),以用于训练模型。借助重排,每个批次可从分布的所有数据中获取各种数据。通过这样做,我们可以帮助模型:

  • 不学习纯粹依赖于数据输入顺序的东西
  • 对子组中的结构不敏感(例如,如果模型在训练的前半部分仅看到高马力汽车,可能会学习一种不适用于数据集其余部分的关系)。

转换为张量

// Step 2. Convert data to Tensor
const inputs = data.map(d => d.horsepower)
const labels = data.map(d => d.mpg);

const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

我们会创建两个数组,一个用于输入样本(马力条目),另一个用于真实输出值(在机器学习中称为标签)。

然后,将每个数组数据转换为 2d 张量。张量的形状将为 [num_examples, num_features_per_example]。我们使用的是 inputs.length 样本,每个样本都具有 1 输入特征(马力)。

对数据进行归一化

//Step 3. Normalize the data to the range 0 - 1 using min-max scaling
const inputMax = inputTensor.max();
const inputMin = inputTensor.min();
const labelMax = labelTensor.max();
const labelMin = labelTensor.min();

const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

接下来,我们将实施另一种最佳做法来进行机器学习训练。我们会对数据进行归一化。我们使用最小-最大缩放比例将数据归一化为数值范围 0-1归一化至关重要,因为您将使用 tensorflow.js 构建的许多机器学习模型的内部构件旨在处理不太大的数字。对数据进行归一化以包含 0 to 1-1 to 1 的通用范围。如果您养成将数据归一化到某合理范围内的习惯,那么在训练模型时就更有可能取得成功。

返回数据和归一化边界

return {
  inputs: normalizedInputs,
  labels: normalizedLabels,
  // Return the min/max bounds so we can use them later.
  inputMax,
  inputMin,
  labelMax,
  labelMin,
}

我们希望保留训练期间用于归一化的值,以便我们可以将输出取消归一化,以使其恢复到原始比例,并且使我们能以相同方式对今后的输入数据进行归一化。

创建模型实例并将数据表示为张量之后,我们就可以开始训练过程了。

96914ff65fc3b74c.png 将以下函数复制到您的

script.js 文件中。

async function trainModel(model, inputs, labels) {
  // Prepare the model for training.
  model.compile({
    optimizer: tf.train.adam(),
    loss: tf.losses.meanSquaredError,
    metrics: ['mse'],
  });

  const batchSize = 32;
  const epochs = 50;

  return await model.fit(inputs, labels, {
    batchSize,
    epochs,
    shuffle: true,
    callbacks: tfvis.show.fitCallbacks(
      { name: 'Training Performance' },
      ['loss', 'mse'],
      { height: 200, callbacks: ['onEpochEnd'] }
    )
  });
}

我们来详细介绍一下。

为训练做好准备

// Prepare the model for training.
model.compile({
  optimizer: tf.train.adam(),
  loss: tf.losses.meanSquaredError,
  metrics: ['mse'],
});

在我们训练模型之前,我们必须对其进行“编译”。为此,我们必须指定一些非常重要的事项:

  • optimizer:这是用于控制模型更新的算法,如样本所示。TensorFlow.js 中提供了许多优化器。我们选择了 Adam 优化器,因为它在实际使用中非常有效,无需进行任何配置。
  • loss:这是一个函数,用于告知模型在学习所显示的各个批次(数据子集)时的表现如何。我们使用 meanSquaredError 将模型所做的预测与真实值进行比较。
const batchSize = 32;
const epochs = 50;

接下来,我们将选择 batchSize 和多个周期:

  • batchSize 是指模型在每次训练迭代时会看到的数据子集的大小。常见的批次大小通常介于 32-512 之间。对于所有问题,实际上并没有理想的批次大小,并且描述各种批次大小的数学动机超出了本教程的范围。
  • epochs 表示模型查看您提供的整个数据集的次数。我们将对数据集执行 50 次迭代。

启动训练循环

return await model.fit(inputs, labels, {
  batchSize,
  epochs,
  callbacks: tfvis.show.fitCallbacks(
    { name: 'Training Performance' },
    ['loss', 'mse'],
    { height: 200, callbacks: ['onEpochEnd'] }
  )
});

model.fit 是您为了启动训练循环而调用的函数。这是一个异步函数,因此我们会返回它提供的 promise,以便调用方确定训练何时完成。

为了监控训练进度,我们会将一些回调传递给 model.fit。我们使用 tfvis.show.fitCallbacks 来生成可为我们之前指定的“损失”和“均方误差'”指标绘制图表的函数。

综合应用

现在,我们必须调用通过 run 函数定义的函数。

96914ff65fc3b74c.png 将以下代码添加到

run 函数的底部。

// Convert the data to a form we can use for training.
const tensorData = convertToTensor(data);
const {inputs, labels} = tensorData;

// Train the model
await trainModel(model, inputs, labels);
console.log('Done Training');

刷新页面几秒钟后,您应该会看到以下图表更新。

c6d3214d6e8c3752.png

这是由我们之前创建的回调创建的。它们显示每个周期结束时整个数据集的平均损失和均方误差。

在训练模型时,我们希望看到损失下降。在本例中,由于我们的指标是用来衡量错误的,因此我们还希望看到该指标下降。

现在模型已经过训练,我们需要进行一些预测。让我们查看模型对均匀范围内的数字(从低到高的马力)的预测情况,从而对其进行评估。

96914ff65fc3b74c.png 将以下函数添加到您的 script.js 文件中

function testModel(model, inputData, normalizationData) {
  const {inputMax, inputMin, labelMin, labelMax} = normalizationData;

  // Generate predictions for a uniform range of numbers between 0 and 1;
  // We un-normalize the data by doing the inverse of the min-max scaling
  // that we did earlier.
  const [xs, preds] = tf.tidy(() => {

    const xs = tf.linspace(0, 1, 100);
    const preds = model.predict(xs.reshape([100, 1]));

    const unNormXs = xs
      .mul(inputMax.sub(inputMin))
      .add(inputMin);

    const unNormPreds = preds
      .mul(labelMax.sub(labelMin))
      .add(labelMin);

    // Un-normalize the data
    return [unNormXs.dataSync(), unNormPreds.dataSync()];
  });

  const predictedPoints = Array.from(xs).map((val, i) => {
    return {x: val, y: preds[i]}
  });

  const originalPoints = inputData.map(d => ({
    x: d.horsepower, y: d.mpg,
  }));

  tfvis.render.scatterplot(
    {name: 'Model Predictions vs Original Data'},
    {values: [originalPoints, predictedPoints], series: ['original', 'predicted']},
    {
      xLabel: 'Horsepower',
      yLabel: 'MPG',
      height: 300
    }
  );
}

在上述函数中需要注意的一些事项。

const xs = tf.linspace(0, 1, 100);
const preds = model.predict(xs.reshape([100, 1]));

我们生成了 100 个新“样本”,以提供给模型。Model.predict 是我们将这些样本提供给模型的方式。请注意,它们必须具有与训练时相似的形状 ([num_examples, num_features_per_example])。

// Un-normalize the data
const unNormXs = xs
  .mul(inputMax.sub(inputMin))
  .add(inputMin);

const unNormPreds = preds
  .mul(labelMax.sub(labelMin))
  .add(labelMin);

要将数据恢复到原始范围(而非 0-1),我们会使用归一化过程中计算的值,但只是进行逆运算。

return [unNormXs.dataSync(), unNormPreds.dataSync()];

.dataSync() 是一种用于获取张量中存储的值的 typedarray 的方法。这使我们能够在常规 JavaScript 中处理这些值。这是通常首选的 .data() 方法的同步版本。

最后,我们使用 tfjs-vis 来绘制原始数据和模型的预测。

96914ff65fc3b74c.png 将以下代码添加到您的

run 函数中。

// Make some predictions using the model and compare them to the
// original data
testModel(model, data, tensorData);

刷新页面,您应会在模型完成训练后看到如下内容。

fe610ff34708d4a.png

恭喜!您刚刚训练了一个简单的机器学习模型。目前,它会执行所谓的线性回归,从而尝试将一条线与输入数据中的趋势拟合。

训练机器学习模型的步骤包括:

制定任务:

  • 是回归问题还是分类问题?
  • 可以通过监督式学习还是非监督式学习来完成?
  • 输入数据的形状是什么?输出数据应该是什么样的?

准备数据:

  • 清理数据并尽可能手动检查它是否存在任何模式
  • 在使用数据进行训练之前对数据进行重排
  • 将数据归一化为神经网络的合理范围。通常,对于数值数据,0-1 或 -1-1 是合适的范围。
  • 将数据转换为张量

构建并运行您的模型:

  • 使用 tf.sequentialtf.model 定义模型,然后使用 tf.layers.* 向模型中添加层
  • 选择优化器(adam 通常是一个不错的选择),以及批次大小和周期数等参数。
  • 为您的问题选择合适的损失函数,并选择准确率指标来帮助您评估进度。meanSquaredError 是处理回归问题的常见损失函数。
  • 监控训练,看看损失是否降低

评估模型

  • 为您的模型选择一个评估指标,您可以在训练过程中对模型进行监控。训练完成后,请尝试进行一些测试预测,以了解预测质量。
  • 更改周期数的实验。在图表数据变化趋缓之前,您需要经历的周期数。
  • 尝试增加隐藏层中的单元数量。
  • 尝试在我们添加的第一个隐藏层与最终输出层之间添加更多隐藏层。这些额外层的代码应如下所示。
model.add(tf.layers.dense({units: 50, activation: 'sigmoid'}));

关于这些隐藏层最重要的一条新信息是它们引入了非线性激活函数(在这种情况下是 sigmoid 激活函数)。如需详细了解激活函数,请参阅这篇文章

看看是否可以使模型生成如下图所示的输出。

a21c5e6537cf81d.png