摘要: 送你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!儘量減少模型的大小對於將模型權重快速傳遞到客戶端應用程序相當重要,特別是若是咱們基本上能夠免費得到它。
本文做者:【方向】
本文爲雲棲社區原創內容,未經容許不得轉載。