介绍
deeplearn.js是用于机器智能的开源WebGL加速JavaScript库。 deeplearn.js将高性能的机器学习构建块带到您的指尖,让您可以在浏览器中训练神经网络或在推断模式(inference mode)下运行预先训练的模型。 它提供了一个用于构建可微数据流图的API,以及可以直接使用的一组数学函数。
您可以在这里找到补充本教程的代码。
运行:
./scripts/watch-demo demos/intro/intro.ts
然后访问http://localhost:8080/demos/intro/
。
或者直接点击这里观看我们的演示。
此文档将使用TypeScript代码示例。 对于vanilla JavaScript,您可能需要删除某些TypeScript类型的注释或定义。
核心概念
NDArrays
deeplearn.js中的中心数据单元是NDArray
。 一个NDArray
由一组浮点值组成,它们是一个任意数量的数组。 NDArray
有一个shape
属性来定义它们的形状。 该库为低级NDArrays
提供糖类:Scalar
,Array1D
,Array2D
,Array3D
和Array4D
。
使用2x3矩阵的示例:
const shape = [2, 3]; // 2行,3列
const a = Array2D.new(shape, [1.0, 2.0, 3.0, 10.0, 20.0, 30.0]);
NDArray
可以在GPU上存储数据作为WebGLTexture
,其中每个像素存储一个浮点值,或者作为一个vanilla JavaScript TypedArray
在CPU上存储数据。 大多数时候,用户不应该考虑存储,因为它是一个实现上的细节。
如果NDArray
数据存储在CPU上,则首次调用GPU数学运算时,数据将自动上传到纹理。 如果在GPU驻留的NDArray
上调用NDArray.getValues()
,库将把纹理下载到CPU并删除纹理。
NDArrayMath
该库提供了一个NDArrayMath
基类,它定义了一组对NDArray
进行操作的数学函数。
NDArrayMathGPU
当使用NDArrayMathGPU
实现时,这些数学运算将着色器程序排列在GPU上执行。 与NDArrayMathCPU
不同,这些操作并不阻止,但用户可以通过在NDArray
上调用get()
或getValues()
来同步cpu和gpu,如下所述。
这些着色器从NGArray
拥有的WebGLTextures
中读取和写入。 当接入数学运算时,纹理可以停留在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]));
// 非阻塞数学调用。
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);
// 阻止调用实际从平均值读取值。
// 等待直到GPU返回值之前完成执行操作。
// average是一个标量,所以我们使用.get()
console.log(average.get());
});
注意:
NDArray.get()
和NDArray.getValues()
正在阻止调用。 执行被链接的数学函数后无需注册回调,只需调用getValues()
来同步CPU和GPU。
math.scope()
当使用数学运算时,必须将它们包装在一个math.scope()
函数闭包中,如上面的例子所示。 此范围内的数学运算结果将被放置在范围的末尾,除非它们是范围中返回的值。
两个函数传递给函数闭包,keep()
和track()
。
keep()
确保在范围结束时,传递给保留的NDArray将不会自动清除。
track()
跟踪可以在范围内直接构造的任何NDArray。 当范围结束时,任何手动跟踪的NDArray
将被清理。 所有math.method()
函数的结果以及许多其他核心库函数的结果都会自动清除,因此您不必手动跟踪它们。
const math = new NDArrayMathGPU();
let output;
// 您必须拥有一个外部范围,但不用担心,如果没有该库,则会导致错误。
math.scope((keep, track) => {
// 正确:默认情况下,数学不会跟踪直接构造的NDArray。
// 您可以在NDArray上调用track(),以便在范围结束时进行跟踪和清理。
const a = track(Scalar.new(2));
// 错误:这是纹理泄漏!
// 数学不知道b,所以它不能跟踪它。 当范围结束时,GPU驻留的NDArray不会被清理,即使b超出范围。
// 确保您在创建的NDArrays上调用track()。
// scope. Make sure you call track() on NDArrays you create.
const b = Scalar.new(2);
// 正确:默认情况下,数学跟踪数学函数的所有输出。
const c = math.neg(math.exp(a));
// 正确:d由父范围跟踪。
const d = math.scope(() => {
// 正确:当内部范围结束时,e将被清理。
const e = track(Scalar.new(3));
// 正确:
// 这个数学功能的结果已经被跟踪。
// 由于它是该范围的返回值,它将不会被内部范围清理。
// 结果将在父范围内自动跟踪。
return math.elementWiseMul(e, e);
});
// 正确但是请注意:math.tanh的输出将被自动跟踪,但是我们可以在其上调用keep(),以便在范围结束时保留它。
// 这意味着如果您稍后调用output.dispose()时不小心,可能会引入纹理内存泄漏。
// 一个更好的方法是将此值作为范围的返回值返回,以便在父作用域中进行跟踪。
output = keep(math.tanh(d));
});
更多技术细节:当WebGL纹理超出JavaScript范围时,它们不会被浏览器的垃圾收集机制自动清理。 这意味着当你完成一个GPU驻留的NDArray,它必须在一段时间后手动放置。 如果您在完成NDArray后忘记手动调用
ndarray.dispose()
,那么您将会引入纹理内存泄漏,从而导致严重的性能问题。 如果使用math.scope()
,则由math.method()
创建的任何NDArray和通过范围返回结果的任何其他方法将自动被清除。
如果要进行手动内存管理,而不使用
math.scope()
,则可以使用safeMode = false构造NDArrayMath
对象。 这是不推荐的,但对于NDArrayMathCPU
是有用的,因为CPU驻留的内存将被JavaScript垃圾回收器自动清理。
NDArrayMathCPU
当使用CPU实现时,这些数学运算被阻塞,并使用vanilla JavaScript立即在底层TypedArray
上执行。
训练
deeplearn.js中的可微数据流图使用延迟执行模型,就像在TensorFlow中一样。 用户构建Graph
,然后通过FeedEntry
提供输入NDArray
来对其进行训练或推断。
注意:NDArrayMath和NDArrays足以推断模式。 如果你想训练,你只需要一个图形(Graph)。
图形(Graph)和张量(Tensor)
Graph
对象是构建数据流图的核心类。Graph
对象实际上并不包含NDArray
数据,只能在操作之间进行连接。
Graph
类具有可操作的操作作为顶级成员函数。 当您调用Graph
方法来添加操作时,您将返回一个仅保存连接和形状信息的Tensor
对象。
一个将输入与变量相乘的示例图:
const g = new Graph();
// 占位符是输入容器。
// 这是在我们执行图形(graph)时我们将为我们传送输入NDArray的容器。
const inputShape = [3];
const inputTensor = g.placeholder('input', inputShape);
const labelShape = [1];
const labelTensor = g.placeholder('label', labelShape);
// 变量是容纳可以从培训中更新的值的容器。
// 这里我们随机初始化乘数变量。
const multiplier = g.variable('multiplier', Array2D.randNormal([1, 3]));
// 最高级图形(graph)方法采用Tensor并返回Tensor。
const outputTensor = g.matmul(multiplier, inputTensor);
const costTensor = g.meanSquaredCost(outputTensor, labelTensor);
// Tensor,像NDArray,有一个shape属性。
console.log(outputTensor.shape);
会话(Session)和 FeedEntry
会话对象用于驱动Graph
的执行。 FeedEntry
(类似于TensorFlow的feed_dict
)为运行提供数据,从给定的NDArray向Tensor
提供值。
关于批处理的一个快速注意事项:deeplearn.js尚未实现批处理作为操作的外部维度。 这意味着每个最高级图形(Graph)操作以及数学函数都可以在单个示例中进行操作。 但是,批处理是很重要的,因此权重更新是根据批次的平均梯度进行操作的。deeplearn.js通过在训练
FeedEntry
中使用InputProvider
来模拟批处理,以直接提供输入,而不是NDArray
。InputProvider
将在批处理中调用每个项目。 我们提供InMemoryShuffledInputProviderBuilder
来混洗一组输入并保持它们同步。
用上面的Graph
对象进行训练:
const learningRate = .00001;
const batchSize = 3;
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([4.0]),
Array1D.new([40.0]),
Array1D.new([400.0])
];
// 混合输入和标签,并保持它们相互同步。
const shuffledInputProviderBuilder =
new InCPUMemoryShuffledInputProviderBuilder([inputs, labels]);
const [inputProvider, labelProvider] =
shuffledInputProviderBuilder.getInputProviders();
// 将张量映射到InputProviders。
const feedEntries: FeedEntry[] = [
{tensor: inputTensor, data: inputProvider},
{tensor: labelTensor, data: labelProvider}
];
const NUM_BATCHES = 10;
for (let i = 0; i < NUM_BATCHES; i++) {
// 在会话中包装session.train,以便自动清除成本。
math.scope(() => {
// 训练需要一个成本张量来最小化。
// 训练一批。返回平均成本作为标量。
const cost = session.train(
costTensor, feedEntries, batchSize, optimizer, CostReduction.MEAN);
console.log('last average cost (' + i + '): ' + cost.get());
});
}
训练后,我们可以通过Graph
推断:
// 在会话中包含session.eval,以便中间值被自动清理。
math.scope((keep, track) => {
const testInput = track(Array1D.new([0.1, 0.2, 0.3]));
// session.eval可以将NDArray作为输入数据。
const testFeedEntries: FeedEntry[] = [
{tensor: inputTensor, data: testInput}
];
const testOutput = session.eval(outputTensor, testFeedEntries);
console.log('---inference output---');
console.log('shape: ' + testOutput.shape);
console.log('value: ' + testOutput.get(0));
});
// 清理训练数据。
inputs.forEach(input => input.dispose());
labels.forEach(label => label.dispose());