摘要: 送你18个训练Tensorflow.js模型的小技巧!
训练神经网络的一个常见建议是经过在每一个时期开始时对输入进行混洗来随机化训练样本。咱们可使用tf.utils.shuffle来实现这个目的:git
/** Shuffles the array using Fisher-Yates algorithm. */ export function shuffle(array: any[]|Uint32Array|Int32Array|Float32Array): void
因为咱们在浏览器中训练咱们的模型,你如今可能会问本身:咱们如何在训练时自动保存模型权重的检查点?咱们可使用FileSaver.js,该脚本公开了一个名为saveAs的函数,咱们可使用它来存储任意类型的文件,这些文件最终会出如今咱们的下载文件夹中。github
这样咱们就能够保存模型权重:数据库
const weights = new Float32Array([... model weights, flat array]) saveAs(new Blob([weights]), 'checkpoint_epoch1.weights')
甚至是json文件:json
const losses = { totalLoss: ... } saveAs(new Blob([JSON.stringify(losses)]), 'loss_epoch1.json')
在花费大量时间训练模型以前,你须要确保你的模型实际上要学习是什么,并消除任何潜在的错误来源。若是你不考虑如下提示,你可能会浪费你的时间在训练垃圾上:canvas
若是你将垃圾传递到你的网络,它定会把垃圾扔回你身边。所以,请确保你的输入数据标记正确,并确保你的网络输入符合你的预期。特别是若是你已经规定了一些预处理逻辑,如随机裁剪、填充、平方、居中、平均减法或其余什么,请确保在预处理后进行可视化输入。此外,我强烈建议单元测试这些步骤。浏览器
这听起来像是一项繁琐的额外工做,但它很重要!缓存
如今在大多数状况下,tensorflow.js为你提供了你须要的损失函数。可是,若是你须要实现本身的损失函数,你绝对须要进行单元测试!不久前,我从头开始使用tfjs-core API实现了Yolo v2 dropout函数,以便为网络训练yolo对象检测器。服务器
一般,最好是在训练数据的一小部分上过分拟合,以验证损失是否正在收敛,以及你的模型其实是否在学习一些有用的东西。所以,你应该只选择10到20张训练数据的图像并训练一些时期。一旦损失收敛,对这10到20张图像进行推断并可视化结果:网络
这是一个很是重要的步骤,它将帮助你消除网络实施中的各类错误来源、先后处理逻辑。异步
特别是,若是你正在实现本身的损失函数,你确定要确保,你的模型可以在开始训练以前收敛!
最后,我想给你一些建议,经过考虑一些基本原则,这将有助于你尽量地减小训练时间并防止浏览器因内存泄漏而崩溃。
除非你是tensorflow.js的新手,不然你可能已经知道,咱们必须手动处理未使用的张量来释放内存,方法是调用tensor.dispose()或将咱们的操做包装在tf.tidy块中。确保因为未正确处理张量而致使没有此类内存泄漏,不然你的应用程序早晚会耗尽内存。
识别这些类型的内存泄漏很是简单,只需记录tf.memory()几回迭代便可验证,每次迭代时张量的数量不会无心中增加:
注意,如下语句仅在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 }
不要过度批量输入!尝试不一样的批量大小并测量反向传播所需的时间。最佳批量大小显然取决于你的GPU统计信息,输入大小以及网络的复杂程度。在某些状况下,你根本不想批量输入。
若是有疑问的话,我会一直使用1的批量大小。我我的认为,在某些状况下,增长批量大小对性能没有任何帮助,但在其余状况下,我能够看到总体加速的因素经过建立大小为16到24的批次,在至关小的网络尺寸下输入图像大小为112x112像素,大约1.5-2.0左右。
咱们的训练图像可能至关大,可能高达1GB甚至更大,具体取决于图像的大小和数量。因为咱们不能简单地在浏览器中从磁盘读取图像,咱们将使用文件代理(多是一个简单的快速服务器)来托管咱们的训练数据,浏览器将获取每一个数据项。
显然,这是很是低效的,可是在浏览器中进行训练时咱们必须记住这一点,若是你的数据集足够小,你可能会尝试将整个数据保存在内存中,但这显然也不是颇有效。最初,我试图增长浏览器缓存大小以简单地将整个数据缓存在磁盘上,但这在之后的Chrome版本中彷佛再也不起做用,并且我也没有运气FireFox。
最后,我决定只使用Indexeddb,这是一个浏览器数据库,你可能不太熟悉,咱们能够利用它来存储咱们的整个训练和测试数据集。Indexeddb入门很是简单,由于咱们基本上只需几行代码便可将整个数据存储和查询为键值存储。使用Indexeddb,咱们能够方便地将标签存储为普通的json对象,将咱们的图像数据存储为blob。看看这篇博文,很好地解释了如何在Indexeddb中保存图像数据和其余文件。
查询Indexeddb是很是快的,至少我发现查询每一个数据项的速度要快一些,而不是一遍又一遍地从代理服务器中获取文件。此外,在将数据移动到Indexeddb以后,技术上的训练如今彻底脱机,这意味着咱们可能再也不须要代理服务器了。
这是一个简单但很是有效的提示,它帮助我减小了训练时的迭代次数。主要的做用是,若是咱们想要检索由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) }
一旦咱们完成了对模型的训练而且咱们对它的性能感到满意,我建议经过应用权重量化来缩小模型大小。经过量化咱们的模型权重,咱们能够将模型的大小减少到原始大小的1/4!尽量减少模型的大小对于将模型权重快速传递到客户端应用程序相当重要,特别是若是咱们基本上能够免费得到它。
本文做者:【方向】
本文为云栖社区原创内容,未经容许不得转载。