TensorFlow.js 入门指南
2018 年 4 月 3 日
作者: Zaid Alyafeai
TensorFlow js banner

Tensorflow.js 是基于 deeplearn.js 构建的库,用于直接在浏览器中创建深度学习模块。使用它,您可以在浏览器上创建 CNN、RNN 等,并使用客户端的 GPU 处理能力训练这些模块。因此,训练 NN 不需要服务器 GPU。本教程首先解释 TensorFlow.js 的基本构建块及其操作,然后介绍如何创建一些复杂的模型。

一两个说明…

如果您想尝试代码,我在 Observable 上创建了一个交互式编码会话。此外,我还创建了许多 小型项目,包括简单的分类、风格迁移、姿态估计和 pix2pix 转换。

入门

由于 TensorFlow.js 在浏览器上运行,您只需要将以下脚本包含到 html 文件的标题中即可
<script src="https://cdn.jsdelivr.net.cn/npm/@tensorflow/tfjs@latest"> </script>
这将加载最新发布的捆绑包版本。

张量(构建块)

如果您熟悉 TensorFlow 等深度学习平台,您应该能够识别出张量是*n*维数组,由运算符使用。因此,它们代表了任何深度学习应用程序的构建块。让我们创建一个标量张量
const tensor = tf.scalar(2);
这创建了一个标量张量。我们也可以将数组转换为张量
const input = tf.tensor([2,2]);
这将创建一个数组[2,2] 的常量张量。换句话说,我们通过应用张量函数将一维数组转换为张量。我们可以使用input.shape 来检索张量的大小
const tensor_s = tf.tensor([2,2]).shape;
这具有形状[2]。我们还可以创建具有特定大小的张量。例如,这里我们创建一个形状为[2,2] 的零张量。
const input = tf.zeros([2,2]);

运算符

为了使用张量,我们需要对其进行操作。假设我们想要找到张量的平方
const a = tf.tensor([1,2,3]);
a.square().print();
x2 的值将为[4,9,16]。TensorFlow.js 还允许链接操作。例如,要计算张量的 2 次方,我们使用
const x = tf.tensor([1,2,3]);
const x2 = x.square().square();
x2 张量的值将为[1,16,81]

张量处理

通常,我们会生成许多中间张量。例如,在前面的示例中,在计算x2 之后,我们不再需要x 的值。为了做到这一点,我们调用dispose()
const x = tf.tensor([1,2,3]);
x.dispose();
请注意,我们不能再在后续操作中使用张量x。现在,对每个张量都这样做可能有点不方便。实际上,不处理张量会给内存带来开销。TensorFlow.js 提供了一个特殊的运算符tidy() 来自动处理中间张量
function f(x)
{
 return tf.tidy(()=>{
  const y = x.square();
  const z = x.mul(y);
  return z
        });
}
请注意,张量y 的值将被处理掉,因为在计算z 的值后,我们不再需要它。

优化问题

在这里,我们学习如何解决优化问题。给定函数f(x),要求我们计算x = a,使其最小化f(x)。为此,我们需要一个优化器。优化器是一种通过遵循梯度来最小化函数的算法。文献中有很多优化器,如 SGD、Adam 等…这些优化器在速度和准确性方面有所不同。Tensorflowjs 支持最重要的优化器

我们将以一个简单的例子为例,其中f(x) = x⁶+2x⁴+3x²+x+1. 函数的图形如下所示。我们看到函数的最小值在区间[-0.5,0] 中。我们将使用一个优化器来找到确切的值。
函数 f(x) 的图形
首先,我们定义要最小化的函数
function f(x) 
{
  const f1 = x.pow(tf.scalar(6, 'int32')) //x^6
  const f2 = x.pow(tf.scalar(4, 'int32')).mul(tf.scalar(2)) //2x^4
  const f3 = x.pow(tf.scalar(2, 'int32')).mul(tf.scalar(3)) //3x^2
  const f4 = tf.scalar(1) //1
  return f1.add(f2).add(f3).add(x).add(f4)
}
现在,我们可以迭代地最小化函数以找到最小值。我们将从a = 2 的初始值开始。学习率定义了我们跳跃以到达最小值的快慢。我们将使用 Adam 优化器
function minimize(epochs , lr)
{
  let y = tf.variable(tf.scalar(2)) //initial value 
  const optim = tf.train.adam(lr);  //gadient descent algorithm 
  for(let i = 0 ; i < epochs ; i++) //start minimiziation 
    optim.minimize(() => f(y));
  return y 
}
使用学习率为0.9 的值,我们在200 次迭代后发现最小值为-0.16092407703399658

简单的神经网络

现在我们学习如何创建一个神经网络来学习 XOR,它是一种非线性操作。代码类似于 keras 实现。我们首先创建训练集,它接受两个输入和一个输出。我们将在每次迭代中馈送 4 个项目的批次
xs = tf.tensor2d([[0,0],[0,1],[1,0],[1,1]])
ys = tf.tensor2d([[0],[1],[1],[0]])
然后,我们创建两个具有两个不同非线性激活函数的密集层。我们使用随机梯度下降和交叉熵损失。学习率为0.1
function createModel()
{
  var model = tf.sequential()
  model.add(tf.layers.dense({units:8, inputShape:2, activation: 'tanh'}))
  model.add(tf.layers.dense({units:1, activation: 'sigmoid'}))
  model.compile({optimizer: 'sgd', loss: 'binaryCrossentropy', lr:0.1})
  return model
}
然后,我们将模型拟合5000iterations
  await model.fit(xs, ys, {
       batchSize: 1,
       epochs: 5000
   })
最后,我们在训练集上进行预测
model.predict(xs).print()
输出应该是[[0.0064339], [0.9836861], [0.9835356], [0.0208658]],这是预期的。

CNN 模型

TensorFlow.js 使用计算图 进行自动微分。我们只需要创建层、优化器并编译模型即可。让我们创建一个顺序模型
model = tf.sequential();
现在,我们可以为模型添加不同的层。让我们添加第一个卷积层,其输入为[28,28,1]
const convlayer = tf.layers.conv2d({
  inputShape: [28,28,1],
  kernelSize: 5,
  filters: 8,
  strides: 1,
  activation: 'relu',
  kernelInitializer: 'VarianceScaling'
});
在这里,我们创建了一个conv 层,它接受大小为[28,28,1] 的输入。输入将是大小为28 x 28 的灰度图像。然后,我们应用大小为5x5、步长等于18 个内核,并使用VarianceScaling 初始化。之后,我们应用一个激活函数,该函数基本上会获取张量中的负值并将其替换为零。现在,我们可以将此convlayer 添加到模型中
model.add(convlayer);
现在,Tensorflow.js 的好处是,我们不需要为下一层指定输入大小,因为在编译模型后,它将自动评估。我们也可以添加最大池化、密集层等等。这是一个简单的模型
const model = tf.sequential();

//create the first layer 
model.add(tf.layers.conv2d({
  inputShape: [28, 28, 1],
  kernelSize: 5,
  filters: 8,
  strides: 1,
  activation: 'relu',
  kernelInitializer: 'VarianceScaling'
}));

//create a max pooling layer 
model.add(tf.layers.maxPooling2d({
  poolSize: [2, 2],
  strides: [2, 2]
}));

//create the second conv layer
model.add(tf.layers.conv2d({
  kernelSize: 5,
  filters: 16,
  strides: 1,
  activation: 'relu',
  kernelInitializer: 'VarianceScaling'
}));

//create a max pooling layer 
model.add(tf.layers.maxPooling2d({
  poolSize: [2, 2],
  strides: [2, 2]
}));

//flatten the layers to use it for the dense layers 
model.add(tf.layers.flatten());

//dense layer with output 10 units 
model.add(tf.layers.dense({
  units: 10,
  kernelInitializer: 'VarianceScaling',
  activation: 'softmax'
}));
我们可以将张量应用于任何层以检查输出张量。但这里有一个问题,输入需要是[BATCH_SIZE,28,28,1] 形状,其中BATCH_SIZE 表示我们一次应用于模型的数据集元素数量。这是一个评估卷积层的方法示例
const convlayer = tf.layers.conv2d({
  inputShape: [28, 28, 1],
  kernelSize: 5,
  filters: 8,
  strides: 1,
  activation: 'relu',
  kernelInitializer: 'VarianceScaling'
});

const input = tf.zeros([1,28,28,1]);
const output = convlayer.apply(input);
在检查output 张量的形状后,我们发现它具有形状[1,24,24,8]。这是使用公式评估的
const outputSize = Math.floor((inputSize-kernelSize)/stride +1);
这将导致我们的结果为24。回到我们的模型,我们意识到我们使用了flatten(),它基本上将输入从形状[BATCH_SIZE,a,b,c] 转换为形状[BATCH_SIZE,axbxc]。这很重要,因为在密集层中,我们不能应用2d 数组。最后,我们使用了具有输出单元10 的密集层,它代表了我们的识别系统中所需的类别数量。实际上,此模型用于识别所谓的MNIST 数据集中手写数字。

优化和编译

在创建模型后,我们需要一种方法来优化参数。有很多不同的方法可以做到这一点,比如SGDAdam 优化器。例如,我们可以使用以下方法创建优化器
const LEARNING_RATE = 0.0001;
const optimizer = tf.train.adam(LEARNING_RATE);
这将使用指定的学习率创建一个 Adam 优化器。现在,我们准备编译模型(将模型与优化器连接起来)
model.compile({
  optimizer: optimizer,
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});
在这里,我们创建了一个使用 Adam 优化损失函数的模型,该函数评估预测输出和真实标签的交叉熵

训练

编译模型后,我们准备在数据集上训练模型。我们需要为此使用fit() 函数
const batch = tf.zeros([BATCH_SIZE,28,28,1]);
const labels = tf.zeros([BATCH_SIZE, NUM_CLASSES]);

const h = await model.fit(batch, labels,
            {
              batchSize: BATCH_SIZE,
              validationData: validationData,
              epochs: BATCH_EPOCHs
            });
请注意,我们正在向拟合函数馈送训练集的批次。fit 函数的第二个变量表示模型的真实标签。最后,我们有配置参数,如batchSizeepochs。请注意,epochs 表示我们对当前批次迭代的次数,而不是整个数据集。因此,例如,我们可以将此代码包装在一个for 循环中,该循环遍历训练集的所有批次。

请注意,我们使用了特殊关键字await,它基本上会阻塞并等待函数完成代码执行。就像运行另一个线程,而主线程正在等待拟合函数完成执行。

独热编码

通常,给定的标签是代表类别的数字。例如,假设我们有两个类别:橙色类别和苹果类别。然后,我们将为橙色类别指定标签0,为苹果类别指定标签1。但是,我们的网络接受大小为[BATCH_SIZE,NUM_CLASSES] 的张量。因此,我们需要使用我们所说的独热编码
const output = tf.oneHot(tf.tensor1d([0,1,0]), 2);

//the output will be [[1, 0],[0, 1],[1, 0]]
因此,我们将标签的1d 张量转换为形状为[BATCH_SIZE,NUM_CLASSES] 的张量。

损失和准确率

为了检查模型的性能,我们需要了解损失和准确率。为此,我们需要使用历史模块获取模型的结果
//h is the output of the fitting module
const loss = h.history.loss[0];
const accuracy = h.history.acc[0];
请注意,我们正在评估validationData 的损失和准确率,它是fit() 函数的输入。

预测

假设我们已经完成了模型的训练,并且它提供了良好的损失和准确率。现在是时候预测未见数据的元素的结果了。假设我们得到了一张在浏览器中或直接从网络摄像头获取的图像,那么我们可以使用我们训练过的模型来预测其类别。首先,我们需要将图像转换为张量
//retrieve the canvas
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

//get image data
imageData = ctx.getImageData(0, 0, 28, 28);

//convert to tensor 
const tensor = tf.fromPixels(imageData);
在这里,我们创建了一个canvas,并从中获取了imageData,然后将其转换为张量。现在,张量的大小将为[28,28,3],但模型采用的是四维向量。因此,我们需要使用expandDims为张量添加一个额外的维度。
const eTensor = tensor.expandDims(0);
因此,输出张量的大小将为[1,28,28,3],因为我们在索引0处添加了一个维度。现在,为了预测,我们只需使用predict()
model.predict(eTensor);
函数predict将返回我们网络中最后一层的输出值,通常是一个softmax激活函数。

迁移学习

在前面的部分中,我们必须从头开始训练我们的模型。然而,这是一个昂贵的操作,因为它需要更多的训练迭代。因此,我们使用一个叫做mobilenet的预训练模型。它是一个轻量级的CNN,经过优化可以在移动应用程序中运行。Mobilenet是在ImageNet类别上训练的。基本上,我们已经预先计算了在1,000个不同的类别上训练的激活。

要加载模型,我们使用以下代码:
const mobilenet = await tf.loadModel(
      'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v1_0.25_224/model.json');
我们可以使用输入和输出来检查模型的结构。
//The input size is [null, 224, 224, 3]
const input_s = mobilenet.inputs[0].shape;

//The output size is [null, 1000]
const output_s = mobilenet.outputs[0].shape;
因此,我们需要大小为[1,224,224,3]的图像,输出将是一个大小为[1,1000]的张量,它保存着ImageNet数据集中每个类别的概率。

为了简单起见,我们将使用一个零数组,并尝试从1,000个类别中预测类别编号。
var pred = mobilenet.predict(tf.zeros([1, 224, 224, 3]));
pred.argMax().print();
运行代码后,我得到了类别 = 21,它代表一个风筝。

现在,我们需要检查模型的内容。为此,我们可以获取模型的层和名称。
//The number of layers in the model '88'
const len = mobilenet.layers.length;

//this outputs the name of the 3rd layer 'conv1_relu'
const name3 = mobilenet.layers[3].name;
我们看到我们有88层,如果要在另一个数据集上重新训练这些层,成本会非常高。因此,基本的技巧是仅使用此模型来评估激活(我们不会重新训练),但我们将创建可以训练到其他类别数量的密集层。

例如,假设我们需要一个模型来区分胡萝卜和黄瓜。我们将使用mobilenet模型来计算直到我们选择的某一层的激活。然后,我们使用输出大小为2的密集层来预测正确的类别。因此,mobilenet模型将在某种程度上被“冻结”,我们只需训练密集层。

首先,我们需要删除模型的密集层。我们选择提取一个随机层,比如编号为81,名称为conv_pw_13_relu的层。
const layer = mobilenet.getLayer('conv_pw_13_relu');
现在,让我们更新模型,使其将此层作为输出。
mobilenet = tf.model({inputs: mobilenet.inputs, outputs: layer.output});
最后,我们创建可训练的模型,但我们需要知道最后一层输出的形状。
//this outputs a layer of size [null, 7, 7, 256]
const layerOutput = layer.output.shape;
我们看到形状为[null,7,7,256]。现在,我们可以将此输入到我们的密集层。
 trainableModel = tf.sequential({
    layers: [
      tf.layers.flatten({inputShape: [7, 7, 256]}),
      tf.layers.dense({
        units: 100,
        activation: 'relu',
        kernelInitializer: 'varianceScaling',
        useBias: true
      }),
      tf.layers.dense({
        units: 2,
        kernelInitializer: 'varianceScaling',
        useBias: false,
        activation: 'softmax'
      })
    ]
  });
如您所见,我们创建了一个具有100个神经元的密集层,以及一个大小为2的输出层。
const activation = mobilenet.predict(input);
const predictions = trainableModel.predict(activation);
我们可以使用前面的部分使用某种优化器来训练最后一个模型。

参考

下一篇文章
A Gentle Introduction to TensorFlow.js

作者:Zaid Alyafeai


Tensorflow.js 是一个基于 deeplearn.js 的库,用于直接在浏览器上创建深度学习模块。使用它,您可以在浏览器上创建CNN、RNN等,并使用客户端的GPU处理能力来训练这些模块。因此,训练NN不需要服务器GPU。本教程从解释TensorFlow.js的基本构建块和操作开始…