TensorFlow.js简介

本文翻译自Medium上的文章:A Gentle Introduction to TensorFlow.js,原文地址:https://medium.com/tensorflow/a-gentle-introduction-to-tensorflow-js-dba2e5257702

Tensorflow.js是一个基于deeplearn.js构建的库,可直接在浏览器上创建深度学习模块。使用它可以在浏览器上创建CNN(卷积神经网络)、RNN(循环神经网络)等等,且可以使用终端的GPU处理能力训练这些模型。因此,可以不需要服务器GPU来训练神经网络。本教程首先解释TensorFlow.js的基本构建块及其操作。然后,我们描述了如何创建一些复杂的模型。

一点提示

如果你想体验代码的运行,我在Observable上创建了一个交互式编码会话。此外,我创建了许多小型项目,包括简单分类、样式转换、姿势估计和pix2pix翻译。

入门

由于TensorFlow.js在浏览器上运行,您只需将以下脚本包含在html文件的header部分即可:

<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"> </script>

这将加载最新发布的版本。

张量(构建块)

如果您熟悉TensorFlow之类的深度学习平台,您应该能够认识到张量是操作符使用的n维数组。因此,它们代表了任何深度学习应用程序的构建块。让我们创建一个标量张量:

const tensor = tf.scalar(2);

这创造了一个标量张量。我们还可以将数组转换为张量:

const input = tf.tensor([2,2]);

这会产生数组[2,2]的常量张量。换句话说,我们通过使用tensor函数将一维数组转换为张量。我们可以使用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的值为[1,4,9]。TensorFlow.js还允许链式操作。例如,要评估我们使用的张量的二次幂

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。但是,对于每个张量来说都要调用dispose(),这可能有点不方便。实际上,不释放张量将成为内存负担。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]。我们将使用优化器来找出确切的值。

首先,我们定义要最小化的函数:

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
}

接下来,我们对模型进行5000次迭代拟合:

  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'
});

在这里,我们创建了一个输入大小为[28,28,1]的conv层。输入将是一个大小为28x28的灰色图像。然后我们应用8个尺寸为5x5的核,将stride等于1,并使用VarianceScaling初始化。之后,我们应用一个激活函数ReLU。现在我们可以将此conv层添加到模型中:

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);

在检查输出张量的形状后,我们看到它有形状[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数据集中的手写数字。

优化和编译

创建模型之后,我们需要一种方法来优化参数。有不同的方法可以做到这一点,比如SGD和Adam优化器。例如,我们可以使用:

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函数提供了一批训练集。fit函数的第二个变量表示模型的真实标签。最后,我们有配置参数,如批量大小和epoch。注意,epochs表示我们迭代当前批次(而不是整个数据集)的次数。因此,我们可以将代码放在迭代训练集的所有批次的for循环中。

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

One Hot编码

通常给定的标签是代表类的数字。例如,假设我们有两个类:一个橙色类和一个苹果类。然后我们会给橙色的类标签0和苹果的类标签1。但是,我们的网络接受一个大小为[BATCH_SIZE,NUM_CLASSES]的张量。因此,我们需要使用所谓的one hot编码

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]的张量。

损失和精度

为了检验模型的性能,我们需要知道损失和精度。为了做到这一点,我们需要使用history模块获取模型的结果

//h is the output of the fitting module
const loss = h.history.loss[0];
const accuracy = h.history.acc[0];

注意,我们正在计算作为fit()函数输入的validationData的损失和精度。

预测

我们完成了对模型的训练,得到了良好的损失和精度,是时候预测未知的数据元素的结果了。假设我们在浏览器中有一个图像或者我们直接从网络摄像头中获取,然后我们可以使用训练好的模型来预测它的类别。首先,我们需要把图像转换成张量

//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);

在这里,我们创建了一个画布并从中获得图像数据,然后转换成一个张量。现在张量的大小是[28,28,3],但是模型需要四维向量。因此,我们需要使用expandDims为张量增加一个额外的维度:

const eTensor = tensor.expandDims(0);

这样,输出张量的大小为[1,28,28,3],因为我们在索引0处添加了一个维度。现在,我们只需要使用predict()进行预测:

model.predict(eTensor);

函数predict会返回网络中最后一层,通常是softmax激活函数,的值。

转移学习

在前面的部分中,我们必须从头开始训练我们的模型。然而,这个代价有点大,因为它需要相当多的训练迭代。因此,我们使用了一个预先训练好的名为mobilenet的模型。它是一个轻量级的CNN,经过优化,可以运行在移动应用程序中。Mobilenet基于ImageNet类别进行训练。实际上,它是在1000个分类上进行了训练。

使用如下代码加载模型:

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数据集中每个类的概率。

简单起见,我们将取一个0数组,并尝试预测1000个分类中的类别:

var pred = mobilenet.predict(tf.zeros([1, 224, 224, 3]));
pred.argMax().print();

运行代码后,我得到类别=21,这代表一个风筝o:

现在我们需要检查模型的内容,这样,我们可以得到模型层和名称:

//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个层,这在另一个数据集中再次训练代价是非常大的。因此,最基本的技巧是使用这个模型来评估激活(我们不会重新训练),但是我们将创建密集层,在其他一些类别上进行训练。

例如,假设我们需要一个模型来区分胡萝卜和黄瓜。我们将使用mobilene tmodel来计算我们选择的某个层的激活参数,然后我们使用输出大小为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,7256],现在我们可以将它输入到密集层中:

 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);

我们可以使用前面的部分的方法,使用特定的优化器来训练最后一个模型。

参考

  1. https://js.tensorflow.org/
  2. https://github.com/tensorflow/tfjs-examples

原文发布于微信公众号 - 云水木石(ourpoeticlife)

原文发表时间:2018-08-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券