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);
我们可以使用前面的部分的方法, 使用特定的优化器来训练最后一个模型.
来源: http://www.tuicool.com/articles/ZZfmeqr