TensorFlow.js - 使用 CNN 识别手写数字

在本教程中,我们将构建一个 TensorFlow.js 模型,以使用卷积神经网络识别手写数字。首先,我们将通过让分类器“观察”数千个手写数字图片及其标签来训练分类器。然后,我们会使用模型从未见过的测试数据来评估该分类器的准确性。

该任务被视为分类任务,因为我们会训练模型以将类别(出现在图片中的数字)分配给输入图片。我们将通过显示输入的多个示例和正确的输出来训练模型。这称为监督式学习

您将构建的模型

您将创建一个使用 TensorFlow.js 在浏览器中训练模型的网页。在我们提供特定尺寸的黑白图片后,该模型将对出现在图片中的数字进行分类。相关步骤如下:

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

您将学到的内容

  • TensorFlow.js 语法,用于使用 TensorFlow.js Layers API 创建卷积模型。
  • 在 TensorFlow.js 中制定分类任务
  • 如何使用 tfjs-vis 库监控浏览器内训练。

所需的条件

您还应该熟悉我们的第一个训练教程中的资料。

创建 HTML 网页并添加 JavaScript

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

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TensorFlow.js Tutorial</title>

  <!-- Import TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.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 data file -->
  <script src="data.js" type="module"></script>

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

</head>

<body>
</body>
</html>

为数据和代码创建 JavaScript 文件

  1. 在上述 HTML 文件所在的文件夹中,创建一个名为 data.js 的文件,并将此链接中的内容复制到该文件中。
  2. 在与第 1 步相同的文件夹中,创建一个名为 script.js 的文件,并将以下代码放入其中。
console.log('Hello TensorFlow');

开始测试

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

如果一切正常,系统应创建两个全局变量。tf 是对 TensorFlow.js 库的引用,tfvis 是对 tfjs-vis 库的引用。

您应该会看到一条显示 Hello TensorFlow** 的消息。如果是这样,那么您可以继续执行下一步操作。

在本教程中,您将训练一个模型,学习识别图片中的数字(如下所示)。这些图片是名为 MNIST 的数据集中的 28x28 像素灰度图片。

mnist 4 mnist 3 mnist 8

我们提供了代码,用于从我们为您创建的特殊精灵文件(大约 10MB)中加载这些图片,以便我们可以将精力集中在训练部分。

您可以随时学习 data.js 文件,以了解数据的加载方式。完成本教程后,请创建您自己的数据加载方法。

提供的代码包含 MnistData 类,它具有以下两种公开方法:

  • nextTrainBatch(batchSize):从训练集返回随机批次的图片及其标签。
  • nextTestBatch(batchSize):从测试集中返回一批图片及其标签

MnistData 类还执行了重排数据和将数据归一化的重要步骤。

总共有 65000 张图片,我们最多可使用 55000 张来训练模型,并保存 10000 张图片,用于在操作完成后测试模型的性能。我们在浏览器中即可完成上述所有操作!

让我们加载数据并测试数据是否已正确加载。

96914ff65fc3b74c.png 将以下代码添加到您的 script.js 文件中。

import {MnistData} from './data.js';

async function showExamples(data) {
  // Create a container in the visor
  const surface =
    tfvis.visor().surface({ name: 'Input Data Examples', tab: 'Input Data'});

  // Get the examples
  const examples = data.nextTestBatch(20);
  const numExamples = examples.xs.shape[0];

  // Create a canvas element to render each example
  for (let i = 0; i < numExamples; i++) {
    const imageTensor = tf.tidy(() => {
      // Reshape the image to 28x28 px
      return examples.xs
        .slice([i, 0], [1, examples.xs.shape[1]])
        .reshape([28, 28, 1]);
    });

    const canvas = document.createElement('canvas');
    canvas.width = 28;
    canvas.height = 28;
    canvas.style = 'margin: 4px;';
    await tf.browser.toPixels(imageTensor, canvas);
    surface.drawArea.appendChild(canvas);

    imageTensor.dispose();
  }
}

async function run() {
  const data = new MnistData();
  await data.load();
  await showExamples(data);
}

document.addEventListener('DOMContentLoaded', run);

刷新页面,几秒钟后,左侧会显示一个面板,其中包含许多图片。

6dff857738b54eed.png

输入数据如下所示。

6dff857738b54eed.png

我们的目标是训练一个模型,该模型会获取一张图片,然后学习预测图片可能所属的 10 个类中每个类的得分(数字 0-9)。

每张图片的尺寸为 28x28 像素,并具有 1 个颜色通道,因为这是灰度图片。因此,每张图片的形状为 [28, 28, 1]

请注意,我们要进行一对十的映射并设置每个输入示例的形状,因为这对于下一部分非常重要。

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

在机器学习中,我们定义了架构(或算法),并让训练过程学习该算法的参数。

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

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

function getModel() {
  const model = tf.sequential();

  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const IMAGE_CHANNELS = 1;

  // In the first layer of our convolutional neural network we have
  // to specify the input shape. Then we specify some parameters for
  // the convolution operation that takes place in this layer.
  model.add(tf.layers.conv2d({
    inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
    kernelSize: 5,
    filters: 8,
    strides: 1,
    activation: 'relu',
    kernelInitializer: 'varianceScaling'
  }));

  // The MaxPooling layer acts as a sort of downsampling using max values
  // in a region instead of averaging.
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));

  // Repeat another conv2d + maxPooling stack.
  // Note that we have more filters in the convolution.
  model.add(tf.layers.conv2d({
    kernelSize: 5,
    filters: 16,
    strides: 1,
    activation: 'relu',
    kernelInitializer: 'varianceScaling'
  }));
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));

  // Now we flatten the output from the 2D filters into a 1D vector to prepare
  // it for input into our last layer. This is common practice when feeding
  // higher dimensional data to a final classification output layer.
  model.add(tf.layers.flatten());

  // Our last layer is a dense layer which has 10 output units, one for each
  // output class (i.e. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9).
  const NUM_OUTPUT_CLASSES = 10;
  model.add(tf.layers.dense({
    units: NUM_OUTPUT_CLASSES,
    kernelInitializer: 'varianceScaling',
    activation: 'softmax'
  }));

  // Choose an optimizer, loss function and accuracy metric,
  // then compile and return the model
  const optimizer = tf.train.adam();
  model.compile({
    optimizer: optimizer,
    loss: 'categoricalCrossentropy',
    metrics: ['accuracy'],
  });

  return model;
}

让我们更详细地了解此方面。

卷积

model.add(tf.layers.conv2d({
  inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
  kernelSize: 5,
  filters: 8,
  strides: 1,
  activation: 'relu',
  kernelInitializer: 'varianceScaling'
}));

我们现在使用顺序模型。

我们使用 conv2d 层而不是密集层。我们无法详细介绍卷积的工作原理的所有细节,但提供了以下资源,可供您了解基本操作:

让我们深入了解 conv2d 的配置对象中的每个参数:

  • inputShape。将流入模型第一层的数据的形状。在本例中,我们的 MNIST 示例是 28x28 像素的黑白图片。图片数据的规范格式为 [row, column, depth],因此在这里我们需要配置以下形状:[28, 28, 1]。各个维度的像素数量为 28 行和 28 列,深度为 1,因为我们的图片只有一个颜色通道。请注意,我们不会在输入形状中指定批次大小。层设计为与批次大小无关,因此在推理期间,您可以传入任何批次大小的张量。
  • kernelSize。要应用于输入数据的滑动卷积过滤器窗口的尺寸。在此示例中,我们将 5 设为 kernelSize,以指定方形的 5x5 卷积窗口。
  • filters。要应用于输入数据的尺寸为 kernelSize 的过滤器窗口数量。在此示例中,我们将对数据应用 8 个过滤器。
  • strides。滑动窗口的“步长”,即每次移动图片时过滤器都会移动多少像素。我们指定步长为 1,表示过滤器将以 1 像素为步长在图片上滑动。
  • activation。卷积完成后应用于数据的激活函数。在本例中,我们将应用修正线性单元 (ReLU) 函数,这是机器学习模型中非常常见的激活函数。
  • kernelInitializer。用于随机初始化模型权重的方法,这对于训练动态非常重要。我们在这里不详细介绍初始化,但此处使用的 VarianceScaling 通常是一种很好的初始化程序选择

展平数据表示法

model.add(tf.layers.flatten());

图片是高维数据,而卷积运算往往会增大传入其中的数据的大小。在将数据传递到最终分类层之前,我们需要将数据展平为一个长数组。密集层(我们会用作最终层)只需要采用 tensor1d,因而此步骤在许多分类任务中很常见。

计算我们的最终概率分布

const NUM_OUTPUT_CLASSES = 10;
model.add(tf.layers.dense({
  units: NUM_OUTPUT_CLASSES,
  kernelInitializer: 'varianceScaling',
  activation: 'softmax'
}));

我们将使用具有 softmax 激活的密集层计算 10 个可能的类的概率分布。得分最高的类将是预测的数字。

选择优化器和损失函数

const optimizer = tf.train.adam();
model.compile({
  optimizer: optimizer,
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});

我们编译了模型,以指定要跟踪的优化程序损失函数和指标。

与第一个教程不同,我们在这里使用 categoricalCrossentropy 作为损失函数。顾名思义,当模型的输出为概率分布时,就会使用此函数。categoricalCrossentropy 会衡量模型的最后一层生成的概率分布与真实标签提供的概率分布之间的误差。

例如,如果我们的数字实际上代表 7,那么结果可能如下所示

索引

0

1 次

2

3

4

5

6

7

8

9

真实标签

0

0

0

0

0

0

0

1

0

0

预测

0.1

0.01

0.01

0.01

0.20

0.01

0.01

0.60

0.03

0.02

分类交叉熵会生成一个数字,指示预测向量与真实标签向量的相似程度。

此处用于标签的数据表示法称为独热编码,在分类问题中很常见。对于每个示例,每个类都有相关联的概率。如果我们确切地知道应该如何进行设置,就可以将这种概率设为 1,而将其他值设为 0。如需详细了解独热编码,请参阅此页面

我们监控的另一个指标是 accuracy,对于分类问题,这是正确预测在所有预测中所占的百分比。

96914ff65fc3b74c.png 将以下函数复制到您的 script.js 文件中。

async function train(model, data) {
  const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];
  const container = {
    name: 'Model Training', tab: 'Model', styles: { height: '1000px' }
  };
  const fitCallbacks = tfvis.show.fitCallbacks(container, metrics);

  const BATCH_SIZE = 512;
  const TRAIN_DATA_SIZE = 5500;
  const TEST_DATA_SIZE = 1000;

  const [trainXs, trainYs] = tf.tidy(() => {
    const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
    return [
      d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
      d.labels
    ];
  });

  const [testXs, testYs] = tf.tidy(() => {
    const d = data.nextTestBatch(TEST_DATA_SIZE);
    return [
      d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
      d.labels
    ];
  });

  return model.fit(trainXs, trainYs, {
    batchSize: BATCH_SIZE,
    validationData: [testXs, testYs],
    epochs: 10,
    shuffle: true,
    callbacks: fitCallbacks
  });
}

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

run 函数。

const model = getModel();
tfvis.show.modelSummary({name: 'Model Architecture', tab: 'Model'}, model);

await train(model, data);

刷新页面,几秒钟后,系统会显示一些报告训练进度的图表。

a2c7628dc47d465.png

下面我们来详细介绍一下。

监控指标

const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];

我们将确定要监控的指标。我们将监控训练集的损失和准确率,以及验证集的损失和准确率(val_loss 和 acc_acc)。我们将在下文详细介绍的验证集。

准备作为张量的数据

const BATCH_SIZE = 512;
const TRAIN_DATA_SIZE = 5500;
const TEST_DATA_SIZE = 1000;

const [trainXs, trainYs] = tf.tidy(() => {
  const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
  return [
    d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
    d.labels
  ];
});

const [testXs, testYs] = tf.tidy(() => {
  const d = data.nextTestBatch(TEST_DATA_SIZE);
  return [
    d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
    d.labels
  ];
});

在这里,我们创建了两个数据集:一个用于训练模型的训练集,一个用于在每个周期结束时测试模型的验证集。不过,在训练过程中,验证集中的数据绝不会向模型展示。

我们提供的数据类可让您轻松从图片数据中获取张量。但是,我们仍然会将这些张量重塑为模型所需的形状 ([num_examples, image_width, image_height, channels]),然后才能将其提供给模型。对于每个数据集,我们都有输入 (X) 和标签 (Y)。

return model.fit(trainXs, trainYs, {
  batchSize: BATCH_SIZE,
  validationData: [testXs, testYs],
  epochs: 10,
  shuffle: true,
  callbacks: fitCallbacks
});

我们调用 model.fit 来启动训练循环。我们还会传递 validationData 属性,以指明模型在每个周期后使用哪些数据来测试本身(但不用于训练)。

如果我们在使用训练数据时的表现良好,但在使用验证数据时表现不佳,则意味着模型很可能与训练数据过拟合,并且对从未出现过的输入泛化效果不佳。

验证准确率能够很好地预估模型对之前未出现过的数据的效果(只要该数据在某种程度上类似于验证集)。但是,我们可能需要更详细地了解不同类的性能。

tfjs-vis 中有一些方法可以帮助您实现上述目标。

96914ff65fc3b74c.png 将以下代码添加到 script.js 文件的底部

const classNames = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];

function doPrediction(model, data, testDataSize = 500) {
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const testData = data.nextTestBatch(testDataSize);
  const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
  const labels = testData.labels.argMax(-1);
  const preds = model.predict(testxs).argMax(-1);

  testxs.dispose();
  return [preds, labels];
}

async function showAccuracy(model, data) {
  const [preds, labels] = doPrediction(model, data);
  const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
  const container = {name: 'Accuracy', tab: 'Evaluation'};
  tfvis.show.perClassAccuracy(container, classAccuracy, classNames);

  labels.dispose();
}

async function showConfusion(model, data) {
  const [preds, labels] = doPrediction(model, data);
  const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
  const container = {name: 'Confusion Matrix', tab: 'Evaluation'};
  tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});

  labels.dispose();
}

此代码有何用途?

  • 进行预测。
  • 计算准确率指标。
  • 显示指标

接下来我们来详细了解每个步骤。

做出预测

function doPrediction(model, data, testDataSize = 500) {
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const testData = data.nextTestBatch(testDataSize);
  const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
  const labels = testData.labels.argMax(-1);
  const preds = model.predict(testxs).argMax(-1);

  testxs.dispose();
  return [preds, labels];
}

首先,我们需要进行一些预测。我们将拍摄 500 张图片并预测其中包含的数字(您可以稍后增大该数字以对更大的图片集进行测试)。

值得注意的是,argmax 函数为我们提供概率最高的类的索引。请注意,模型会输出每个类的概率。我们会找出最高概率,并指定将其用作预测。

您可能还注意到,我们可以一次对全部 500 个示例进行预测。这是 TensorFlow.js 提供的矢量化功能。

显示每个类的准确率

async function showAccuracy() {
  const [preds, labels] = doPrediction();
  const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
  const container = { name: 'Accuracy', tab: 'Evaluation' };
  tfvis.show.perClassAccuracy(container, classAccuracy, classNames);

  labels.dispose();
}

借助一组预测和标签,我们可以计算每个类的准确率。

显示混淆矩阵

async function showConfusion() {
  const [preds, labels] = doPrediction();
  const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
  const container = { name: 'Confusion Matrix', tab: 'Evaluation' };
  tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});

  labels.dispose();
}

混淆矩阵与每个类的准确率相似,但会进一步细分以显示错误分类的模式。借助混淆矩阵,您可以了解模型是否对任何特定的类对感到困惑。

显示评估

96914ff65fc3b74c.png 将以下代码添加到运行函数的底部,可显示评估。

await showAccuracy(model, data);
await showConfusion(model, data);

您应该会看到如下所示的显示内容。

82458197bd5e7f52.png

恭喜!您刚刚训练了一个卷积神经网络!

预测输入数据类别的任务称为分类任务。

分类任务需要使用适当的数据表示法来显示标签。

  • 标签的常见表示法包括对类别进行独热编码

准备数据:

  • 将模型在训练期间从未见过并且您可以用于评估模型的一些数据单独保存会很有帮助。这称为验证集。

构建并运行您的模型:

  • 事实证明,卷积模型很适合处理图片任务。
  • 分类问题通常使用分类交叉熵作为其损失函数。
  • 监控训练,看看损失是否减少,准确率是否上升。

评估模型

  • 确定一些方式,在模型经过训练后对其进行评估,以了解其在处理您要解决的最初问题时表现如何。
  • 每个类准确率和混淆矩阵可以为您提供比整体准确率更详细的模型性能。