谷歌开源DeepLearn.js:可在网页上实现硬件加速的机器学习

业界资讯 来源:机器之心 发布:2017-08-10 浏览:1566

摘要:deeplearn.js 是一个可用于机器智能并加速 WebGL 的开源 JavaScript 库。deeplearn.js 提供高效的机器学习构建模块,使我们能够在浏览器中训练神经网络或在推断模式中运行预训练模型。它提供构建可微数据流图的 API,以及一系列可直接使用的数学函数。

本文经机器之心(微信公众号:almosthuman2014)授权转载,禁止二次转载。

deeplearn.js 是一个可用于机器智能并加速 WebGL 的开源 JavaScript 库。deeplearn.js 提供高效的机器学习构建模块,使我们能够在浏览器中训练神经网络或在推断模式中运行预训练模型。它提供构建可微数据流图的 API,以及一系列可直接使用的数学函数。

本文档中,我们使用 TypeScript 代码示例。对于 vanilla JavaScript,你可能需要移除 TypeScript 语法,如 const、let 或其他类型定义。


核心概念

NDArrays

deeplearn.js 的核心数据单元是 NDArray。NDArray 包括一系列浮点值,它们将构建为任意维数的数组。NDArray 具备一个用来定义形状的 shape 属性。该库为低秩 NDArray 提供糖子类(sugar subclasses):Scalar、Array1D、Array2D、Array3D 和 Array4D。

2x3 矩阵的用法示例:

const shape = [2, 3];  // 2 rows, 3 columns
const a = Array2D.new(shape, [1.0, 2.0, 3.0, 10.0, 20.0, 30.0]);

NDArray 可作为 WebGLTexture 在 GPU 上储存数据,每一个像素储存一个浮点值;或者作为 vanilla JavaScript TypedArray 在 CPU 上储存数据。大多数时候,用户不应思考储存问题,因为这只是一个实现细节。

如果 NDArray 数据储存在 CPU 上,那么 GPU 数学操作第一次被调用时,该数据将被自动上传至一个 texture。如果你在 GPU 常驻内存的 NDArray 上调用 NDArray.getValues(),则该库将下载该 texture 至 CPU,然后将其删除。

NDArrayMath

该库提供一个 NDArrayMath 基类,其为定义在 NDArray 上运行的一系列数学函数。

NDArrayMathGPU

当使用 NDArrayMathGPU 实现时,这些数学运算对将在 GPU 上执行的着色器程序(shader program)排序。和 NDArrayMathCPU 中不同,这些运算不会阻塞,但用户可以通过在 NDArray 上调用 get() 或 getValues() 使 cpu 和 gpu 同步化,详见下文。

这些着色器从 NDArray 上的 WebGLTexture 中读取和写入。当连接数学运算时,纹理可以停留在 GPU 内存中(不必下载至运算之间的 CPU),这对性能来说非常关键。

以两个矩阵间的均方差为例(有关 math.scope、keep 和 track 的更多细节,详见下文):

const math = new NDArrayMathGPU();

math.scope((keep, track) => {
  const a = track(Array2D.new([2, 2], [1.0, 2.0, 3.0, 4.0]));
  const b = track(Array2D.new([2, 2], [0.0, 2.0, 4.0, 6.0]));

  // Non-blocking math calls.
  const diff = math.sub(a, b);
  const squaredDiff = math.elementWiseMul(diff, diff);
  const sum = math.sum(squaredDiff);
  const size = Scalar.new(a.size);
  const average = math.divide(sum, size);

  // Blocking call to actually read the values from average. Waits until the
  // GPU has finished executing the operations before returning values.
  console.log(average.get());  // average is a Scalar so we use .get()
});

注:NDArray.get() 和 NDArray.getValues() 是阻塞调用。因此在执行一系列数学函数之后,无需寄存回调函数,只需调用 getValues() 来使 CPU 和 GPU 同步化。

小技巧:避免在 GPU 数学运算之间调用 get() 或 getValues(),除非你在进行调试。因为这会强制下载 texture,然后后续的 NDArrayMathGPU 调用将不得不重新下载数据至新的 texture 中。

math.scope()

当我们进行数学运算时,我们需要像如上所示的案例那样将它们封装到 math.scope() 函数的闭包中。该数学运算的结果将在作用域的端点处得到配置,除非该函数在作用域内返回函数值。

有两种函数可以传递到函数闭包中:keep() 和 track()。

  • keep() 确保了 NDArray 将得到传递并保留,它不会在作用域范围结束后被自动清除。
  • track() 追踪了我们在作用域内直接构建的 NDArray。当作用域结束时,任何手动追踪的 NDArray 都将会被清除。math.method() 函数的结果和其它核心库函数的结果一样将会被自动清除,所以我们也不必手动追踪它们。
const math = new NDArrayMathGPU();

let output;

// You must have an outer scope, but don't worry, the library will throw an
// error if you don't have one.
math.scope((keep, track) => {
  // CORRECT: By default, math wont track NDArrays that are constructed
  // directly. You can call track() on the NDArray for it to get tracked and
  // cleaned up at the end of the scope.
  const a = track(Scalar.new(2));

  // INCORRECT: This is a texture leak!!
  // math doesn't know about b, so it can't track it. When the scope ends, the
  // GPU-resident NDArray will not get cleaned up, even though b goes out of
  // scope. Make sure you call track() on NDArrays you create.
  const b = Scalar.new(2);

  // CORRECT: By default, math tracks all outputs of math functions.
  const c = math.neg(math.exp(a));

  // CORRECT: d is tracked by the parent scope.
  const d = math.scope(() => {
    // CORRECT: e will get cleaned up when this inner scope ends.
    const e = track(Scalar.new(3));

    // CORRECT: The result of this math function is tracked. Since it is the
    // return value of this scope, it will not get cleaned up with this inner
    // scope. However, the result will be tracked automatically in the parent
    // scope.
    return math.elementWiseMul(e, e);
  });

  // CORRECT, BUT BE CAREFUL: The output of math.tanh will be tracked
  // automatically, however we can call keep() on it so that it will be kept
  // when the scope ends. That means if you are not careful about calling
  // output.dispose() some time later, you might introduce a texture memory
  // leak. A better way to do this would be to return this value as a return
  // value of a scope so that it gets tracked in a parent scope.
  output = keep(math.tanh(d));
});

技术细节:当 WebGL textures 在 JavaScript 的作用范围之外时,它们因为浏览器的碎片回收机制而不会被自动清除。这就意味着当我们使用 GPU 常驻内存完成了 NDArray 时,它随后需要手动地配置。如果我们完成 NDArray 时忘了手动调用 ndarray.dispose(),那就会引起 texture 内存渗漏,这将会导致十分严重的性能问题。如果我们使用 math.scope(),任何由 math.method() 创建的 NDArray 或其它通过作用域返回函数值方法创建的 NDArray 都会被自动清除。

如果我们想不使用 math.scope(),并且手动配置内存,那么我们可以令 safeMode = false 来构建 NDArrayMath 对象。这种方法我们并不推荐,但是因为 CPU 常驻内存可以通过 JavaScript 碎片回收器自动清除,所以它对 NDArrayMathCPU 十分有用。

NDArrayMathCPU

当我们使用 CPU 实现模型时,这些数学运算是封闭的并且可以通过 vanilla JavaScript 在底层 TypedArray 上立即执行。

训练

在 deeplearn.js 中的可微数据流图使用的是延迟执行模型,这一点就和 TensorFlow 一样。用户可以通过 FeedEntrys 提供的输入 NDArray 构建一个计算图,然后再在上面进行训练或推断。

注意:NDArrayMath 和 NDArrays 对于推断模式来说是足够的,如果我们希望进行训练,只需要一个图就行。

图和张量

Graph 对象是构建数据流图的核心类别,Graph 对象实际上并不保留 NDArray 数据,它只是在运算中构建连接。

Graph 类像顶层成员函数(member function)一样有可微分运算。当我们调用一个图方法来添加运算时,我们就会获得一个 Tensor 对象,它仅仅保持连通性和形状信息。

下面是一个将输入和变量做乘积的计算图示例:

const g = new Graph();

// Placeholders are input containers. This is the container for where we will
// feed an input NDArray when we execute the graph.
const inputShape = [3];
const inputTensor = g.placeholder('input', inputShape);

const labelShape = [1];
const inputTensor = g.placeholder('label', labelShape);

// Variables are containers that hold a value that can be updated from training.
// Here we initialize the multiplier variable randomly.
const multiplier = g.variable('multiplier', Array2D.randNormal([1, 3]));

// Top level graph methods take Tensors and return Tensors.
const outputTensor = g.matmul(multiplier, inputTensor);
const costTensor = g.meanSquaredCost(outputTensor, labelTensor);

// Tensors, like NDArrays, have a shape attribute.
console.log(outputTensor.shape);

Session 和 FeedEntry

Session 对象是驱动执行计算图的方法,FeedEntry 对象(和 TensorFlow 中的 feed_dict 类似)将提供运行所需的数据,并从给定的 NDArray 中馈送一个值给 Tensor 对象。

批处理简单的注释:deeplearn.js 并没有执行批处理作为运算的外部维度(outer dimension)。这就意味着每一个顶层图运算就像数学函数那样在单个样本上运算。然而,批处理十分重要,以至于权重的更新依赖于每一个批量的梯度均值。deeplearn.js 在训练 FeedEntry 时通过使用 InputerProvider 模拟批处理来提供输入向量,而不是直接使用 NDArray。因此,每一个批量中的每一项都会调用 InputerProvider。我们同样提供了 InMemoryShuffledInputProviderBuilder 来清洗一系列输入并保持它们的同步性。

通过上面的 Graph 对象训练:

const learningRate = .001;
const batchSize = 2;

const math = new NDArrayMathGPU();
const session = new Session(g, math);
const optimizer = new SGDOptimizer(learningRate);

const inputs: Array1D[] = [
  Array1D.new([1.0, 2.0, 3.0]),
  Array1D.new([10.0, 20.0, 30.0]),
  Array1D.new([100.0, 200.0, 300.0])
];

const labels: Array1D[] = [
  Array1D.new([2.0, 6.0, 12.0]),
  Array1D.new([20.0, 60.0, 120.0]),
  Array1D.new([200.0, 600.0, 1200.0])
];

// Shuffles inputs and labels and keeps them mutually in sync.
const shuffledInputProviderBuilder =
   new InCPUMemoryShuffledInputProviderBuilder([inputs, labels]);
const [inputProvider, labelProvider] =
   shuffledInputProviderBuilder.getInputProviders();

// Maps tensors to InputProviders.
const feedEntries: FeedEntry[] = [
  {tensor: inputTensor, data: inputProvider},
  {tensor: labelTensor, data: labelProvider}
];

// Wrap session.train in a scope so the cost gets cleaned up automatically.
math.scope(() => {
  // Train takes a cost tensor to minimize. Trains one batch. Returns the
  // average cost as a Scalar.
  const cost = session.train(
      costTensor, feedEntries, batchSize, optimizer, CostReduction.MEAN);

  console.log('last average cost: ' + cost.get());
});

在训练后,我们就可以通过图进行推断:


// Wrap session.eval in a scope so the intermediate values get cleaned up // automatically. math.scope((keep, track) => { const testInput = track(Array1D.new([1.0, 2.0, 3.0])); // session.eval can take NDArrays as input data. const testFeedEntries: FeedEntry[] = [ {tensor: inputTensor, data: testInput} ]; const testOutput = session.eval(outputTensor, testFeedEntries); console.log('inference output:'); console.log(testOutput.shape); console.log(testOutput.getValues()); });
原    文:机器之心

「Google」都使用了那些技术和工具?他又是怎样从0到1发展起来的?
点击查看「Google」-- 技术栈

免责声明:

  1. SDK.cn遵循行业规范,所有转载文章均征得作者同意授权并明确标注来源和链接。
  2. 我们十分尊重原创作者的付出,本站禁止二次转载如需转载请与原作者取得联系。
  3. 转载SDK.cn的原创文章需注明文章作者、链接和"来源:SDK.cn"并保留文章及标题完整性未经作者同意不得擅自修改。
  4. 作者投稿可能会经SDK.cn适当编辑修改或补充。
推荐工具 意见反馈