我是程序员机器学习你不知道的JavaScript

用浏览器训练Tensorflow.js模型的18个技巧(下)

2018-10-15  本文已影响43人  阿里云云栖号

摘要: 送你18个训练Tensorflow.js模型的小技巧!

用浏览器训练Tensorflow.js模型的18个技巧(上)

8.随机你的输入!

训练神经网络的一个常见建议是通过在每个时期开始时对输入进行混洗来随机化训练样本。我们可以使用tf.utils.shuffle来实现这个目的:

/** Shuffles the array using Fisher-Yates algorithm. */
export function shuffle(array: any[]|Uint32Array|Int32Array|Float32Array): void

9.使用FileSaver.js保存模型检查点

由于我们在浏览器中训练我们的模型,你现在可能会问自己:我们如何在训练时自动保存模型权重的检查点?我们可以使用FileSaver.js,该脚本公开了一个名为saveAs的函数,我们可以使用它来存储任意类型的文件,这些文件最终会出现在我们的下载文件夹中。

这样我们就可以保存模型权重:

const weights = new Float32Array([... model weights, flat array])
saveAs(new Blob([weights]), 'checkpoint_epoch1.weights')

甚至是json文件:

const losses = { totalLoss: ... }
saveAs(new Blob([JSON.stringify(losses)]), 'loss_epoch1.json')

排除故障

在花费大量时间训练模型之前,你需要确保你的模型实际上要学习是什么,并消除任何潜在的错误来源。如果你不考虑以下提示,你可能会浪费你的时间在训练垃圾上:

10.检查输入数据,预处理和后处理逻辑!

如果你将垃圾传递到你的网络,它定会把垃圾扔回你身边。因此,请确保你的输入数据标记正确,并确保你的网络输入符合你的预期。特别是如果你已经规定了一些预处理逻辑,如随机裁剪、填充、平方、居中、平均减法或其他什么,请确保预处理后进行可视化输入。此外,我强烈建议单元测试这些步骤。

这听起来像是一项繁琐的额外工作,但它很重要!

11.检查你的损失函数!

现在在大多数情况下,tensorflow.js为你提供了你需要的损失函数。但是,如果你需要实现自己的损失函数,你绝对需要进行单元测试!不久前,我从头开始使用tfjs-core API实现了Yolo v2 dropout函数,以便为网络训练yolo对象检测器。

12.先装上一个小型数据集!

通常,最好是在训练数据的一小部分上过度拟合,以验证损失是否正在收敛,以及你的模型实际上是否在学习一些有用的东西。因此,你应该只选择10到20张训练数据的图像并训练一些时期。一旦损失收敛,对这10到20张图像进行推断并可视化结果:

这是一个非常重要的步骤,它将帮助你消除网络实施中的各种错误来源、前后处理逻辑。

特别是,如果你正在实现自己的损失函数,你肯定要确保,你的模型能够在开始训练之前收敛!

性能

最后,我想给你一些建议,通过考虑一些基本原则,这将有助于你尽可能地减少训练时间并防止浏览器因内存泄漏而崩溃。

13.防止明显的内存泄漏

除非你是tensorflow.js的新手,否则你可能已经知道,我们必须手动处理未使用的张量来释放内存,方法是调用tensor.dispose()或将我们的操作包装在tf.tidy块中。确保由于未正确处理张量而导致没有此类内存泄漏,否则你的应用程序迟早会耗尽内存。

识别这些类型的内存泄漏非常简单,只需记录tf.memory()几次迭代即可验证,每次迭代时张量的数量不会无意中增长:

14.调整Canvases大小而不是你的张量!

注意,以下语句仅在tfjs-core的当前状态时有效,直到最终得到修复。

这可能听起来有点奇怪:为什么不使用tf.resizeBilinear、tf.pad等将输入张量重塑为所需的网络输入形状?tfjs目前有一个未解决的问题,说明了这个问题。

TLDR:在调用tf.fromPixels之前,要将Canvaes转换为张量,请调整Canvaes的大小,使其具有网络接受的大小,否则你将快速耗尽GPU内存,具体取决于各种不同的输入大小。如果你的训练图像大小都相同,那么这个问题就不那么严重了,但是如果你必须明确调整它们的大小,你可以使用下面的代码片段:

export function imageToSquare(img: HTMLImageElement | HTMLCanvasElement, inputSize: number): HTMLCanvasElement {
  const dims = img instanceof HTMLImageElement 
    ? { width: img.naturalWidth, height: img.naturalHeight }
    : img 
  const scale = inputSize / Math.max(dims.height, dims.width)
  const width = scale * dims.width
  const height = scale * dims.height

  const targetCanvas = document.createElement('canvas')
  targetCanvas .width = inputSize
  targetCanvas .height = inputSize
  targetCanvas.getContext('2d').drawImage(img, 0, 0, width, height)
  return targetCanvas
}

15.确定最佳批量大小

不要过分批量输入!尝试不同的批量大小并测量反向传播所需的时间。最佳批量大小显然取决于你的GPU统计信息,输入大小以及网络的复杂程度。在某些情况下,你根本不想批量输入。

如果有疑问的话,我会一直使用1的批量大小。我个人认为,在某些情况下,增加批量大小对性能没有任何帮助,但在其他情况下,我可以看到整体加速的因素通过创建大小为1624的批次,在相当小的网络尺寸下输入图像大小为112x112像素,大约1.5-2.0左右。

16.缓存、离线存储、Indexeddb

我们的训练图像可能相当大,可能高达1GB甚至更大,具体取决于图像的大小和数量。由于我们不能简单地在浏览器中从磁盘读取图像,我们将使用文件代理(可能是一个简单的快速服务器)来托管我们的训练数据,浏览器将获取每个数据项。

显然,这是非常低效的,但是在浏览器中进行训练时我们必须记住这一点,如果你的数据集足够小,你可能会尝试将整个数据保存在内存中,但这显然也不是很有效。最初,我试图增加浏览器缓存大小以简单地将整个数据缓存在磁盘上,但这在以后的Chrome版本中似乎不再起作用,而且我也没有运气FireFox。

最后,我决定只使用Indexeddb,这是一个浏览器数据库,你可能不太熟悉,我们可以利用它来存储我们的整个训练和测试数据集。Indexeddb入门非常简单,因为我们基本上只需几行代码即可将整个数据存储和查询为键值存储。使用Indexeddb,我们可以方便地将标签存储为普通的json对象,将我们的图像数据存储为blob。看看这篇博文很好地解释了如何在Indexeddb中保存图像数据和其他文件。

查询Indexeddb是非常快的,至少我发现查询每个数据项的速度要快一些,而不是一遍又一遍地从代理服务器中获取文件。此外,在将数据移动到Indexeddb之后,技术上的训练现在完全脱机,这意味着我们可能不再需要代理服务器了。

17.异步丢失报告

这是一个简单但非常有效的提示,它帮助我减少了训练时的迭代次数。主要的作用是,如果我们想要检索由optimizer.minimize返回的损失张量的值,我们肯定会这样做,因为我们想要在训练时跟踪我们的损失,我们希望避免等待损失返回的lose.data()及防止等待CPU和GPU在每次迭代时同步。相反,我们想要执行类似以下的操作来报告迭代的损失值:

const loss = optimizer.minimize(() => {
  const out = net.predict(someInput)
  const loss = tf.losses.meanSquaredError(
    groundTruth,
    out,
    tf.Reduction.MEAN
  )
  return loss
}, true)
loss.data().then(data => {
  const lossValue = data[0]
  window.lossValues[epoch] += (window.lossValues[epoch] || 0) + lossValue
  loss.dispose()
})

我们只需记住,我们的损失现在是异步报告的,所以如果我们想在每个epoch的末尾将整体损失保存到文件中,我们将不得不等待最后的解决方案。我通常只是通过使用setTimeout在一个epoch完成后10秒左右保存整体损失值来解决这个问题:

if (epoch !== startEpoch) {
  // ugly hack to wait for loss datas for that epoch to be resolved
  const previousEpoch = epoch - 1
  setTimeout(() => storeLoss(previousEpoch, window.losses[previousEpoch]), 10000)
}

成功训练模型后

18.权重量化

一旦我们完成了对模型的训练并且我们对它的性能感到满意,我建议通过应用权重量化来缩小模型大小。通过量化我们的模型权重,我们可以将模型的大小减小到原始大小的1/4!尽可能减小模型的大小对于将模型权重快速传递到客户端应用程序至关重要,特别是如果我们基本上可以免费获得它。

本文作者:【方向】

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

上一篇下一篇

猜你喜欢

热点阅读