26

[译] TensorFlow.js 简介

 5 years ago
source link: https://mp.weixin.qq.com/s/aaudWL156X4DsxwHw0LDLA?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

uiUBzei.png!web

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]。我们将使用优化器来找出确切的值。

YJfmmiA.png!web

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

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

VBreEnM.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK