近日,來(lái)自德國(guó)的 Robin Wieruch 發(fā)布了一系列使用 JavaScript 構(gòu)建機(jī)器學(xué)習(xí)的教程,本文將主要介紹使用 JavaScript 實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)的方法。
JavaScript 是一種流行的高級(jí)編程語(yǔ)言,它被世界上的絕大多數(shù)網(wǎng)站所使用,也被所有主流瀏覽器所支持。隨著深度學(xué)習(xí)的火熱,越來(lái)越多開(kāi)發(fā)者開(kāi)始探索使用 JavaScript 實(shí)現(xiàn)人工智能與機(jī)器學(xué)習(xí)算法。近日,來(lái)自德國(guó)的 Robin Wieruch 發(fā)布了一系列使用 JavaScript 構(gòu)建機(jī)器學(xué)習(xí)的教程,本文將主要介紹使用 JavaScript 實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)的方法。
近期,原作者發(fā)表了一系列有關(guān)在 JavaScript 上實(shí)現(xiàn)人工智能和機(jī)器學(xué)習(xí)算法的文章,其中包括:
線性回歸和梯度下降
正規(guī)方程線性回歸
邏輯回歸和梯度下降
這些機(jī)器學(xué)習(xí)算法的實(shí)現(xiàn)是基于 math.js 庫(kù)的線性代數(shù)(如矩陣運(yùn)算)和微分的,你可以在 GitHub 上找到所有這些算法:
如果你發(fā)現(xiàn)其中存在任何缺陷,歡迎對(duì)這個(gè)資源提出自己的改進(jìn),以幫助后來(lái)者。我希望不斷為 web 開(kāi)發(fā)者們提供更多、更豐富的機(jī)器學(xué)習(xí)算法。
就我個(gè)人來(lái)說(shuō),我發(fā)現(xiàn)實(shí)現(xiàn)這些算法在某種程度上是一個(gè)非常具有挑戰(zhàn)性的任務(wù)。特別是當(dāng)你需要在 JavaScript 上實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)的前向和反向傳播的時(shí)候。由于我自己也在學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)的知識(shí),我開(kāi)始尋找適用于這種工作的庫(kù)。希望在不久的將來(lái),我們能夠輕松地在 GitHub 上找到相關(guān)的基礎(chǔ)實(shí)現(xiàn)。然而現(xiàn)在,以我使用 JavaScript 的閱歷,我選擇了谷歌發(fā)布的 deeplearn.js 來(lái)進(jìn)行此項(xiàng)工作。在本文中,我將分享使用 deeplearn.js 和 JavaScript 實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)從而解決現(xiàn)實(shí)世界問(wèn)題的方式——在 web 環(huán)境上。
首先,我強(qiáng)烈推薦讀者先學(xué)習(xí)一下深度學(xué)習(xí)著名學(xué)者吳恩達(dá)的《機(jī)器學(xué)習(xí)》課程。本文不會(huì)詳細(xì)解釋機(jī)器學(xué)習(xí)算法,只會(huì)展示它在 JavaScript 上的用法。另一方面,該系列課程在算法的細(xì)節(jié)和解釋上有著令人驚嘆的高質(zhì)量。在寫(xiě)這篇文章之前,我自己也學(xué)習(xí)了相關(guān)課程,并試圖用 JavaScript 實(shí)現(xiàn)來(lái)內(nèi)化課程中的相關(guān)知識(shí)。
神經(jīng)網(wǎng)絡(luò)的目的是什么?
本文實(shí)現(xiàn)的神經(jīng)網(wǎng)絡(luò)需要通過(guò)選擇與背景顏色相關(guān)的適當(dāng)字體顏色來(lái)改善網(wǎng)頁(yè)可訪問(wèn)性。比如,深藍(lán)色背景中的字體應(yīng)該是白色,而淺黃色背景中的字體應(yīng)該是黑色。你也許會(huì)想:首先你為什么需要一個(gè)神經(jīng)網(wǎng)絡(luò)來(lái)完成任務(wù)?通過(guò)編程的方式根據(jù)背景顏色計(jì)算可使用的字體顏色并不難,不是嗎?我很快在 Stack Overflow 找到了該問(wèn)題的解決辦法,并根據(jù)我的需求做了調(diào)整,以適應(yīng) RGB 空間中的顏色。
function getAccessibleColor(rgb) {
let [ r, g, b ] = rgb;
let colors = [r / 255, g / 255, b / 255];
let c = colors.map((col) => {
if (col <= 0.03928) {
return col / 12.92;
}
return Math.pow((col + 0.055) / 1.055, 2.4);
});
let L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
return (L > 0.179)
? [ 0, 0, 0 ]
: [ 255, 255, 255 ];
}
當(dāng)已經(jīng)有一個(gè)編程的方法可以解決該問(wèn)題的時(shí)候,使用神經(jīng)網(wǎng)絡(luò)對(duì)于該現(xiàn)實(shí)世界問(wèn)題價(jià)值并不大,沒(méi)有必要使用一個(gè)機(jī)器訓(xùn)練的算法。然而,由于可通過(guò)編程解決這一問(wèn)題,所以驗(yàn)證神經(jīng)網(wǎng)絡(luò)的性能也變得很簡(jiǎn)單,這也許能夠解決我們的問(wèn)題。查看該 GitHub 庫(kù)(https://github.com/javascript-machine-learning/color-accessibility-neural-network-deeplearnjs)中的動(dòng)圖,了解它最終表現(xiàn)如何,以及本教程中你將構(gòu)建什么。如果你熟悉機(jī)器學(xué)習(xí),也許你已經(jīng)注意到這個(gè)任務(wù)是一個(gè)分類問(wèn)題。算法應(yīng)根據(jù)輸入(背景顏色)決定二進(jìn)制輸出(字體顏色:白色或黑色)。在使用神經(jīng)網(wǎng)絡(luò)訓(xùn)練算法的過(guò)程中,最終會(huì)根據(jù)輸入的背景顏色輸出正確的字體顏色。
下文將從頭開(kāi)始指導(dǎo)你設(shè)置神經(jīng)網(wǎng)絡(luò)的所有部分,并由你決定把文件/文件夾設(shè)置中的部分合在一起。但是你可以整合以前引用的 GitHub 庫(kù)以獲取實(shí)現(xiàn)細(xì)節(jié)。
JavaScript 中的數(shù)據(jù)集生成
? ? ? ? 機(jī)器學(xué)習(xí)中的訓(xùn)練集由輸入數(shù)據(jù)點(diǎn)和輸出數(shù)據(jù)點(diǎn)(標(biāo)簽)組成。它被用來(lái)訓(xùn)練為訓(xùn)練集(例如測(cè)試集)之外的新輸入數(shù)據(jù)點(diǎn)預(yù)測(cè)輸出的算法。在訓(xùn)練階段,由神經(jīng)網(wǎng)絡(luò)訓(xùn)練的算法調(diào)整其權(quán)重以預(yù)測(cè)輸入數(shù)據(jù)點(diǎn)的給定標(biāo)簽??傊延?xùn)練算法是一個(gè)以數(shù)據(jù)點(diǎn)作為輸入并近似輸出標(biāo)簽的函數(shù)。
? ? ? ?該算法經(jīng)過(guò)神經(jīng)網(wǎng)絡(luò)的訓(xùn)練后,可以為不屬于訓(xùn)練集的新背景顏色輸出字體顏色。因此,稍后你將使用測(cè)試集來(lái)驗(yàn)證訓(xùn)練算法的準(zhǔn)確率。由于我們正在處理顏色,因此為神經(jīng)網(wǎng)絡(luò)生成輸入顏色的樣本數(shù)據(jù)集并不困難。
function generateRandomRgbColors(m) {
const rawInputs = [];
for (let i = 0; i < m; i++) {
rawInputs.push(generateRandomRgbColor());
}
return rawInputs;
}
function generateRandomRgbColor() {
return [
randomIntFromInterval(0, 255),
randomIntFromInterval(0, 255),
randomIntFromInterval(0, 255),
];
}
function randomIntFromInterval(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
generateRandomRgbColors() 函數(shù)創(chuàng)建給定大小為 m 的部分?jǐn)?shù)據(jù)集。數(shù)據(jù)集中的數(shù)據(jù)點(diǎn)是 RGB 顏色空間中的顏色。每種顏色在矩陣中被表征為一行,而每一列是顏色的特征。特征是 RGB 空間中的 R、G、B 編碼值。數(shù)據(jù)集還沒(méi)有任何標(biāo)簽,所以訓(xùn)練集并不完整,因?yàn)樗挥休斎胫刀鴽](méi)有輸出值。
由于基于已知顏色生成可使用字體顏色的編程方法是已知的,因此可以使用調(diào)整后的功能版本以生成訓(xùn)練集(以及稍后的測(cè)試集)的標(biāo)簽。這些標(biāo)簽針對(duì)二分類問(wèn)題進(jìn)行了調(diào)整,并在 RGB 空間中隱含地反映了黑白的顏色。因此,對(duì)于黑色,標(biāo)簽是 [0,1];對(duì)于白色,標(biāo)簽是 [1,0]。
function getAccessibleColor(rgb) {
let [ r, g, b ] = rgb;
let color = [r / 255, g / 255, b / 255];
let c = color.map((col) => {
if (col <= 0.03928) {
return col / 12.92;
}
return Math.pow((col + 0.055) / 1.055, 2.4);
});
let L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
return (L > 0.179)
? [ 0, 1 ] // black
: [ 1, 0 ]; // white
}
現(xiàn)在你已經(jīng)準(zhǔn)備好一切用于生成(背景)顏色的隨機(jī)數(shù)據(jù)集(訓(xùn)練集、測(cè)試集),它被分類為黑色或白色(字體)顏色。
function generateColorSet(m) {
const rawInputs = generateRandomRgbColors(m);
const rawTargets = rawInputs.map(getAccessibleColor);
return { rawInputs, rawTargets };
}
使神經(jīng)網(wǎng)絡(luò)中底層算法更好的另一步操作是特征縮放。在特征縮放的簡(jiǎn)化版本中,你希望 RGB 通道的值在 0 和 1 之間。由于你知道最大值,因此可以簡(jiǎn)單地推導(dǎo)出每個(gè)顏色通道的歸一化值。
function normalizeColor(rgb) {
return rgb.map(v => v / 255);
}
你可以把這個(gè)功能放在你的神經(jīng)網(wǎng)絡(luò)模型中,或者作為單獨(dú)的效用函數(shù)。下一步我將把它放在神經(jīng)網(wǎng)絡(luò)模型中。
JavaScript 神經(jīng)網(wǎng)絡(luò)模型的設(shè)置階段
現(xiàn)在你可以使用 JavaScript 實(shí)現(xiàn)一個(gè)神經(jīng)網(wǎng)絡(luò)了。在開(kāi)始之前,你需要先安裝 deeplearn.js 庫(kù):一個(gè)適合 JavaScript 神經(jīng)網(wǎng)絡(luò)的框架。官方宣傳中說(shuō):「deeplearn.js 是一個(gè)開(kāi)源庫(kù),將高效的機(jī)器學(xué)習(xí)構(gòu)造塊帶到 web 中,允許在瀏覽器中訓(xùn)練神經(jīng)網(wǎng)絡(luò)或在推斷模式下運(yùn)行預(yù)訓(xùn)練模型?!贡疚模銓⒂?xùn)練自己的模型,然后在推斷模式中運(yùn)行該模型。使用該庫(kù)有兩個(gè)主要優(yōu)勢(shì):
首先,它使用本地電腦的 GPU 加速機(jī)器學(xué)習(xí)算法中的向量計(jì)算。這些機(jī)器學(xué)習(xí)計(jì)算與圖解計(jì)算類似,因此使用 GPU 的計(jì)算比使用 CPU 更加高效。
其次,deeplearn.js 的結(jié)構(gòu)與流行的 TensorFlow 庫(kù)類似(TensorFlow 庫(kù)也是谷歌開(kāi)發(fā)的,不過(guò)它使用的是 Python 語(yǔ)言)。因此如果你想在使用 Python 的機(jī)器學(xué)習(xí)中實(shí)現(xiàn)飛躍,那么 deeplearn.js 可提供通向 JavaScript 各領(lǐng)域的捷徑。
現(xiàn)在回到你的項(xiàng)目。如果你想用 npm 來(lái)設(shè)置,那么你只需要在命令行中安裝 deeplearn.js。也可以查看 deeplearn.js 項(xiàng)目的官方安裝說(shuō)明文檔。
npm install deeplearn
我沒(méi)有構(gòu)建過(guò)大量神經(jīng)網(wǎng)絡(luò),因此我按照構(gòu)建神經(jīng)網(wǎng)絡(luò)的一般實(shí)踐進(jìn)行操作。在 JavaScript 中,你可以使用 JavaScript ES6 class 來(lái)推進(jìn)它。該類可以通過(guò)定義神經(jīng)網(wǎng)絡(luò)特性和類方法為你的神經(jīng)網(wǎng)絡(luò)提供完美的容器。例如,你的顏色歸一化函數(shù)可以在類別中找到一個(gè)作為方法的點(diǎn)。
class ColorAccessibilityModel {
normalizeColor(rgb) {
return rgb.map(v => v / 255);
}
}
export default ColorAccessibilityModel;
或許那也是你的函數(shù)生成數(shù)據(jù)集的地方。在我的案例中,我僅將類別歸一化作為分類方法,讓數(shù)據(jù)集生成獨(dú)立于類別之外。你可以認(rèn)為未來(lái)有不同的方法來(lái)生成數(shù)據(jù)集,不應(yīng)該在神經(jīng)網(wǎng)絡(luò)模型中進(jìn)行定義。不管怎樣,這只是一個(gè)實(shí)現(xiàn)細(xì)節(jié)。
訓(xùn)練和推斷階段都在機(jī)器學(xué)習(xí)的涵蓋性術(shù)語(yǔ)會(huì)話(session)之下。你可以在神經(jīng)網(wǎng)絡(luò)類別中設(shè)置會(huì)話。首先,你可以輸入來(lái)自 deeplearn.js 的 NDArrayMathGPU 類別,幫助你以計(jì)算高效的方式在 GPU 上進(jìn)行數(shù)學(xué)運(yùn)算。
import {
NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
...
}
export default ColorAccessibilityModel;
第二,聲明分類方法類設(shè)置會(huì)話。其函數(shù)簽名使用訓(xùn)練集作為參數(shù),成為從先前實(shí)現(xiàn)的函數(shù)中生成訓(xùn)練集的完美 consumer。
第三步,會(huì)話初始化空的圖。之后,圖將反映神經(jīng)網(wǎng)絡(luò)的架構(gòu)。你可以隨意定義其特性。
import {
Graph,
NDArrayMathGPU,
} from 'deeplearn';
class ColorAccessibilityModel {
setupSession(trainingSet) {
const graph = new Graph();
}
..
}
export default ColorAccessibilityModel;
第四步,你用張量的形式定義圖中輸入和輸出數(shù)據(jù)點(diǎn)的形態(tài)。張量是具備不同維度的數(shù)組,它可以是向量、矩陣,或更高維度的矩陣。神經(jīng)網(wǎng)絡(luò)將這些張量作為輸入和輸出。在我們的案例中,有三個(gè)輸入單元(每個(gè)顏色通道有一個(gè)輸入單元)和兩個(gè)輸出單元(二分類,如黑白)。
class ColorAccessibilityModel {
inputTensor;
targetTensor;
setupSession(trainingSet) {
const graph = new Graph();
this.inputTensor = graph.placeholder('input RGB value', [3]);
this.targetTensor = graph.placeholder('output classifier', [2]);
}
...
}
export default ColorAccessibilityModel;
第五步,神經(jīng)網(wǎng)絡(luò)包含隱藏層。奇跡如何發(fā)生目前仍是黑箱?;旧?,神經(jīng)網(wǎng)絡(luò)提出自己的交叉計(jì)算參數(shù)(在會(huì)話中經(jīng)過(guò)訓(xùn)練)。不過(guò),你可以隨意定義隱藏層的維度(每個(gè)單元大小、層大?。?/p>
class ColorAccessibilityModel {
inputTensor;
targetTensor;
setupSession(trainingSet) {
const graph = new Graph();
this.inputTensor = graph.placeholder('input RGB value', [3]);
this.targetTensor = graph.placeholder('output classifier', [2]);
let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
}
createConnectedLayer(
graph,
inputLayer,
layerIndex,
units,
) {
...
}
...
}
export default ColorAccessibilityModel;
根據(jù)層的數(shù)量,你可以變更圖來(lái)擴(kuò)展出更多層。創(chuàng)建連接層的分類方法需要圖、變異連接層(mutated connected layer)、新層的索引,以及單元數(shù)量。圖的層屬性可用于返回由名稱確定的新張量。
class ColorAccessibilityModel {
inputTensor;
targetTensor;
setupSession(trainingSet) {
const graph = new Graph();
this.inputTensor = graph.placeholder('input RGB value', [3]);
this.targetTensor = graph.placeholder('output classifier', [2]);
let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
}
createConnectedLayer(
graph,
inputLayer,
layerIndex,
units,
) {
return graph.layers.dense(
`fully_connected_${layerIndex}`,
inputLayer,
units
);
}
...
}
export default ColorAccessibilityModel;
神經(jīng)網(wǎng)絡(luò)中的每一個(gè)神經(jīng)元必須具備一個(gè)定義好的激活函數(shù)。它可以是 logistic 激活函數(shù)。你或許已經(jīng)從 logistic 回歸中了解到它,它成為神經(jīng)網(wǎng)絡(luò)中的 logistic 單元。在我們的案例中,神經(jīng)網(wǎng)絡(luò)默認(rèn)使用修正線性單元。
class ColorAccessibilityModel {
inputTensor;
targetTensor;
setupSession(trainingSet) {
const graph = new Graph();
this.inputTensor = graph.placeholder('input RGB value', [3]);
this.targetTensor = graph.placeholder('output classifier', [2]);
let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
}
createConnectedLayer(
graph,
inputLayer,
layerIndex,
units,
activationFunction
) {
return graph.layers.dense(
`fully_connected_${layerIndex}`,
inputLayer,
units,
activationFunction ? activationFunction : (x) => graph.relu(x)
);
}
...
}
export default ColorAccessibilityModel;
第六步,創(chuàng)建輸出二分類的層。它有兩個(gè)輸出單元,每一個(gè)表示一個(gè)離散的值(黑色、白色)。
class ColorAccessibilityModel {
inputTensor;
targetTensor;
predictionTensor;
setupSession(trainingSet) {
const graph = new Graph();
this.inputTensor = graph.placeholder('input RGB value', [3]);
this.targetTensor = graph.placeholder('output classifier', [2]);
let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
this.predictionTensor = this.createConnectedLayer(graph, connectedLayer, 3, 2);
}
...
}
export default ColorAccessibilityModel;
第七步,聲明一個(gè)代價(jià)張量(cost tensor),以定義損失函數(shù)。在這個(gè)案例中,代價(jià)張量是均方誤差。它使用訓(xùn)練集的目標(biāo)張量(標(biāo)簽)和訓(xùn)練算法得到的預(yù)測(cè)張量來(lái)計(jì)算代價(jià)。
class ColorAccessibilityModel {
inputTensor;
targetTensor;
predictionTensor;
costTensor;
setupSession(trainingSet) {
const graph = new Graph();
this.inputTensor = graph.placeholder('input RGB value', [3]);
this.targetTensor = graph.placeholder('output classifier', [2]);
let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
this.predictionTensor = this.createConnectedLayer(graph, connectedLayer, 3, 2);
this.costTensor = graph.meanSquaredCost(this.targetTensor, this.predictionTensor);
}
...
}
export default ColorAccessibilityModel;
最后但并非不重要的一步,設(shè)置架構(gòu)圖的相關(guān)會(huì)話。之后,你就可以開(kāi)始準(zhǔn)備為訓(xùn)練階段導(dǎo)入訓(xùn)練集了。
import {
Graph,
Session,
NDArrayMathGPU,
} from 'deeplearn';
class ColorAccessibilityModel {
session;
inputTensor;
targetTensor;
predictionTensor;
costTensor;
setupSession(trainingSet) {
const graph = new Graph();
this.inputTensor = graph.placeholder('input RGB value', [3]);
this.targetTensor = graph.placeholder('output classifier', [2]);
let connectedLayer = this.createConnectedLayer(graph, this.inputTensor, 0, 64);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 1, 32);
connectedLayer = this.createConnectedLayer(graph, connectedLayer, 2, 16);
this.predictionTensor = this.createConnectedLayer(graph, connectedLayer, 3, 2);
this.costTensor = graph.meanSquaredCost(this.targetTensor, this.predictionTensor);
this.session = new Session(graph, math);
this.prepareTrainingSet(trainingSet);
}
prepareTrainingSet(trainingSet) {
...
}
...
}
export default ColorAccessibilityModel;
不過(guò)目前在準(zhǔn)備神經(jīng)網(wǎng)絡(luò)的訓(xùn)練集之前,設(shè)置還沒(méi)完成。
首先,你可以在 GPU 數(shù)學(xué)計(jì)算環(huán)境中使用回調(diào)函數(shù)(callback function)來(lái)支持計(jì)算,但這并不是強(qiáng)制性的,可自主選擇。
import {
Graph,
Session,
NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
session;
inputTensor;
targetTensor;
predictionTensor;
costTensor;
...
prepareTrainingSet(trainingSet) {
math.scope(() => {
...
});
}
...
}
export default ColorAccessibilityModel;
其次,你可以解構(gòu)訓(xùn)練集的輸入和輸出(標(biāo)簽,也稱為目標(biāo))以將其轉(zhuǎn)換成神經(jīng)網(wǎng)絡(luò)可讀的格式。deeplearn.js 的數(shù)學(xué)計(jì)算使用內(nèi)置的 NDArrays。你可以把它們理解為數(shù)組矩陣中的簡(jiǎn)單數(shù)組或向量。此外,輸入數(shù)組的顏色被歸一化以提高神經(jīng)網(wǎng)絡(luò)的性能。
import {
Array1D,
Graph,
Session,
NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
session;
inputTensor;
targetTensor;
predictionTensor;
costTensor;
...
prepareTrainingSet(trainingSet) {
math.scope(() => {
const { rawInputs, rawTargets } = trainingSet;
const inputArray = rawInputs.map(v => Array1D.new(this.normalizeColor(v)));
const targetArray = rawTargets.map(v => Array1D.new(v));
});
}
...
}
export default ColorAccessibilityModel;
第三,shuffle 輸入和目標(biāo)陣列。shuffle 的時(shí)候,deeplearn.js 提供的 shuffler 將二者保存在 sync 中。每次訓(xùn)練迭代都會(huì)出現(xiàn) shuffle,以饋送不同的輸入作為神經(jīng)網(wǎng)絡(luò)的 batch。整個(gè) shuffle 流程可以改善訓(xùn)練算法,因?yàn)樗赡芡ㄟ^(guò)避免過(guò)擬合來(lái)實(shí)現(xiàn)泛化。
import {
Array1D,
InCPUMemoryShuffledInputProviderBuilder,
Graph,
Session,
NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
session;
inputTensor;
targetTensor;
predictionTensor;
costTensor;
...
prepareTrainingSet(trainingSet) {
math.scope(() => {
const { rawInputs, rawTargets } = trainingSet;
const inputArray = rawInputs.map(v => Array1D.new(this.normalizeColor(v)));
const targetArray = rawTargets.map(v => Array1D.new(v));
const shuffledInputProviderBuilder = new InCPUMemoryShuffledInputProviderBuilder([
inputArray,
targetArray
]);
const [
inputProvider,
targetProvider,
] = shuffledInputProviderBuilder.getInputProviders();
});
}
...
}
export default ColorAccessibilityModel;
最后,饋送條目(feed entries)是訓(xùn)練階段中神經(jīng)網(wǎng)絡(luò)前饋算法的最終輸入。它匹配數(shù)據(jù)和張量(根據(jù)設(shè)置階段的形態(tài)而定義)。
import {
Array1D,
InCPUMemoryShuffledInputProviderBuilder
Graph,
Session,
NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
session;
inputTensor;
targetTensor;
predictionTensor;
costTensor;
feedEntries;
...
prepareTrainingSet(trainingSet) {
math.scope(() => {
const { rawInputs, rawTargets } = trainingSet;
const inputArray = rawInputs.map(v => Array1D.new(this.normalizeColor(v)));
const targetArray = rawTargets.map(v => Array1D.new(v));
const shuffledInputProviderBuilder = new InCPUMemoryShuffledInputProviderBuilder([
inputArray,
targetArray
]);
const [
inputProvider,
targetProvider,
] = shuffledInputProviderBuilder.getInputProviders();
this.feedEntries = [
{ tensor: this.inputTensor, data: inputProvider },
{ tensor: this.targetTensor, data: targetProvider },
];
});
}
...
}
export default ColorAccessibilityModel;
這樣,神經(jīng)網(wǎng)絡(luò)的設(shè)置就結(jié)束了。神經(jīng)網(wǎng)絡(luò)的所有層和單元都實(shí)現(xiàn)了,訓(xùn)練集也準(zhǔn)備好進(jìn)行訓(xùn)練了。現(xiàn)在只需要添加兩個(gè)配置神經(jīng)網(wǎng)絡(luò)行為的超參數(shù),它們適用于下個(gè)階段:訓(xùn)練階段。
import {
Array1D,
InCPUMemoryShuffledInputProviderBuilder,
Graph,
Session,
SGDOptimizer,
NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
session;
optimizer;
batchSize = 300;
initialLearningRate = 0.06;
inputTensor;
targetTensor;
predictionTensor;
costTensor;
feedEntries;
constructor() {
this.optimizer = new SGDOptimizer(this.initialLearningRate);
}
...
}
export default ColorAccessibilityModel;
第一個(gè)參數(shù)是學(xué)習(xí)速率(learning rate)。學(xué)習(xí)速率決定算法的收斂速度,以最小化成本。我們應(yīng)該假定它的數(shù)值很高,但實(shí)際上不能太高了。否則梯度下降就不會(huì)收斂,因?yàn)檎也坏骄植孔顑?yōu)值。
第二個(gè)參數(shù)是批尺寸(batch size)。它定義每個(gè) epoch(迭代)里有多少個(gè)訓(xùn)練集的數(shù)據(jù)點(diǎn)通過(guò)神經(jīng)網(wǎng)絡(luò)。一個(gè) epoch 等于一批數(shù)據(jù)點(diǎn)的一次正向傳播和一次反向傳播。以批次的方式訓(xùn)練神經(jīng)網(wǎng)絡(luò)有兩個(gè)好處:第一,這樣可以防止密集計(jì)算,因?yàn)樗惴ㄓ?xùn)練時(shí)使用了內(nèi)存中的少量數(shù)據(jù)點(diǎn);第二,這樣可以讓神經(jīng)網(wǎng)絡(luò)更快地進(jìn)行批處理,因?yàn)槊總€(gè) epoch 中權(quán)重會(huì)隨著每個(gè)批次的數(shù)據(jù)點(diǎn)進(jìn)行調(diào)整——而不是等到整個(gè)數(shù)據(jù)集訓(xùn)練完之后再進(jìn)行改動(dòng)。
訓(xùn)練階段
設(shè)置階段結(jié)束后就到了訓(xùn)練階段了。不需要太多實(shí)現(xiàn),因?yàn)樗械幕A(chǔ)都已在設(shè)置階段完成。首先,訓(xùn)練階段可以用分類方法來(lái)定義。然后在 deeplearn.js 的數(shù)學(xué)環(huán)境中再次執(zhí)行。此外,它還使用神經(jīng)網(wǎng)絡(luò)實(shí)例所有的預(yù)定義特性來(lái)訓(xùn)練算法。
class ColorAccessibilityModel {
...
train() {
math.scope(() => {
this.session.train(
this.costTensor,
this.feedEntries,
this.batchSize,
this.optimizer
);
});
}
}
export default ColorAccessibilityModel;
訓(xùn)練方法是 1 個(gè) epoch 的神經(jīng)網(wǎng)絡(luò)訓(xùn)練。因此,從外部調(diào)用時(shí),調(diào)用必須是迭代的。此外,訓(xùn)練只需要 1 個(gè) epoch。為了多批次訓(xùn)練算法,你必須將該訓(xùn)練方法進(jìn)行多次迭代運(yùn)行。
這就是基礎(chǔ)的訓(xùn)練階段。但是根據(jù)時(shí)間調(diào)整學(xué)習(xí)率可以改善訓(xùn)練。學(xué)習(xí)率最初很高,但是當(dāng)算法在每一步過(guò)程中逐漸收斂時(shí),學(xué)習(xí)率會(huì)出現(xiàn)下降趨勢(shì)。
class ColorAccessibilityModel {
...
train(step) {
let learningRate = this.initialLearningRate * Math.pow(0.90, Math.floor(step / 50));
this.optimizer.setLearningRate(learningRate);
math.scope(() => {
this.session.train(
this.costTensor,
this.feedEntries,
this.batchSize,
this.optimizer
);
}
}
}
export default ColorAccessibilityModel;
在我們的情況中,學(xué)習(xí)率每 50 步下降 10%。下面,我們需要獲取訓(xùn)練階段的損失,來(lái)驗(yàn)證它是否隨著時(shí)間下降。損失可在每一次迭代時(shí)返回,不過(guò)這樣會(huì)導(dǎo)致較低的計(jì)算效率。神經(jīng)網(wǎng)絡(luò)每次請(qǐng)求返回?fù)p失,就必須通過(guò) GPU 才能實(shí)現(xiàn)返回請(qǐng)求。因此,我們?cè)诙啻蔚髢H要求返回一次損失來(lái)驗(yàn)證其是否下降。如果沒(méi)有請(qǐng)求返回?fù)p失,則訓(xùn)練的損失下降常量被定義為 NONE(之前默認(rèn)設(shè)置)。
import {
Array1D,
InCPUMemoryShuffledInputProviderBuilder,
Graph,
Session,
SGDOptimizer,
NDArrayMathGPU,
CostReduction,
} from 'deeplearn';
class ColorAccessibilityModel {
...
train(step, computeCost) {
let learningRate = this.initialLearningRate * Math.pow(0.90, Math.floor(step / 50));
this.optimizer.setLearningRate(learningRate);
let costValue;
math.scope(() => {
const cost = this.session.train(
this.costTensor,
this.feedEntries,
this.batchSize,
this.optimizer,
computeCost ? CostReduction.MEAN : CostReduction.NONE,
);
if (computeCost) {
costValue = cost.get();
}
});
return costValue;
}
}
export default ColorAccessibilityModel;
最后,這就是訓(xùn)練階段?,F(xiàn)在僅需要在訓(xùn)練集上進(jìn)行會(huì)話設(shè)置后從外部進(jìn)行迭代執(zhí)行。外部的執(zhí)行取決于訓(xùn)練方法是否返回?fù)p失。
推斷階段
最后一個(gè)階段是推斷階段,該階段使用測(cè)試集來(lái)驗(yàn)證訓(xùn)練算法的性能。輸入是背景顏色中的 RGB 顏色,輸出是算法為字體顏色是黑是白進(jìn)行的 [ 0, 1 ] 或 [ 1, 0 ] 分類預(yù)測(cè)。由于輸入數(shù)據(jù)點(diǎn)經(jīng)過(guò)歸一化,因此不要忘記在這一步也對(duì)顏色進(jìn)行歸一化。
class ColorAccessibilityModel {
...
predict(rgb) {
let classifier = [];
math.scope(() => {
const mapping = [{
tensor: this.inputTensor,
data: Array1D.new(this.normalizeColor(rgb)),
}];
classifier = this.session.eval(this.predictionTensor, mapping).getValues();
});
return [ ...classifier ];
}
}
export default ColorAccessibilityModel;
該方法在數(shù)學(xué)環(huán)境中再次運(yùn)行性能關(guān)鍵部分,需要定義一個(gè)映射,該映射最終可作為會(huì)話評(píng)估的輸入。記住,預(yù)測(cè)方法不是一定得在訓(xùn)練階段后運(yùn)行。它可以在訓(xùn)練階段中使用,來(lái)輸出測(cè)試集的驗(yàn)證。至此,神經(jīng)網(wǎng)絡(luò)已經(jīng)經(jīng)歷了設(shè)置、訓(xùn)練和推斷階段。
在 JavaScript 中可視化學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)
現(xiàn)在是時(shí)候使用神經(jīng)網(wǎng)絡(luò)進(jìn)行訓(xùn)練和驗(yàn)證/測(cè)試了。簡(jiǎn)單的過(guò)程為建立一個(gè)神經(jīng)網(wǎng)絡(luò),使用一個(gè)訓(xùn)練集運(yùn)行訓(xùn)練階段,代價(jià)函數(shù)取得最小值之后,使用一個(gè)測(cè)試集進(jìn)行預(yù)測(cè)。所有的過(guò)程只需要使用網(wǎng)頁(yè)瀏覽器上的開(kāi)發(fā)者控制臺(tái)的幾個(gè) console.log statements 就可以完成。然而,由于該神經(jīng)網(wǎng)絡(luò)是關(guān)于顏色預(yù)測(cè)的,并且 deeplearn.js 是在瀏覽器上運(yùn)行,從而可以輕松地對(duì)神經(jīng)網(wǎng)絡(luò)的訓(xùn)練階段和測(cè)試階段進(jìn)行可視化。
至此,你可以自主決定你運(yùn)行中的神經(jīng)網(wǎng)絡(luò)的可視化方式。使用一個(gè) canvas 和 repuestAnimationFrame API 可以使 JavaScript 代碼更簡(jiǎn)單。但就這篇文章來(lái)說(shuō),我會(huì)使用 React.js 進(jìn)行展示,因?yàn)槲以诓┛蜕蠈?xiě)過(guò) React.js。
因此在使用 create-react-app 設(shè)置完項(xiàng)目后,App 組件可成為我們可視化的進(jìn)入點(diǎn)。首先,導(dǎo)入神經(jīng)網(wǎng)絡(luò)類別和函數(shù),從你的文件中生成數(shù)據(jù)集。進(jìn)而,為訓(xùn)練集大小、測(cè)試集大小和訓(xùn)練迭代次數(shù)添加若干個(gè)常量。
import React, { Component } from 'react';
import './App.css';
import generateColorSet from './data';
import ColorAccessibilityModel from './neuralNetwork';
const ITERATIONS = 750;
const TRAINING_SET_SIZE = 1500;
const TEST_SET_SIZE = 10;
class App extends Component {
...
}
export default App;
App 的組件包括生成數(shù)據(jù)集(訓(xùn)練集和測(cè)試集)、通過(guò)傳遞訓(xùn)練集建立神經(jīng)網(wǎng)絡(luò)會(huì)話、定義組件的初始狀態(tài)。在訓(xùn)練階段的時(shí)間內(nèi),代價(jià)函數(shù)的值和迭代次數(shù)會(huì)在控制臺(tái)上顯示,它也表示了組件的狀態(tài)。
import React, { Component } from 'react';
import './App.css';
import generateColorSet from './data';
import ColorAccessibilityModel from './neuralNetwork';
const ITERATIONS = 750;
const TRAINING_SET_SIZE = 1500;
const TEST_SET_SIZE = 10;
class App extends Component {
testSet;
trainingSet;
colorAccessibilityModel;
constructor() {
super();
this.testSet = generateColorSet(TEST_SET_SIZE);
this.trainingSet = generateColorSet(TRAINING_SET_SIZE);
this.colorAccessibilityModel = new ColorAccessibilityModel();
this.colorAccessibilityModel.setupSession(this.trainingSet);
this.state = {
currentIteration: 0,
cost: -42,
};
}
...
}
export default App;
接下來(lái),設(shè)置了神經(jīng)網(wǎng)絡(luò)會(huì)話之后,就可以迭代地訓(xùn)練神經(jīng)網(wǎng)絡(luò)了。最簡(jiǎn)單的版本只需要一直運(yùn)行 React 的一個(gè) for 循環(huán)就可以了。
class App extends Component {
...
componentDidMount () {
for (let i = 0; i <= ITERATIONS; i++) {
this.colorAccessibilityModel.train(i);
}
};
}
export default App;
然而,以上代碼不會(huì)在 React 的訓(xùn)練階段提供(render)輸出,因?yàn)榻M件不會(huì)在神經(jīng)網(wǎng)絡(luò)阻塞單個(gè) JavaScript 線程的時(shí)候 reRender。這也正是 React 使用 requestAnimationFrame 的時(shí)候。與其自己定義一個(gè) for 循環(huán),每一個(gè)請(qǐng)求的瀏覽器的動(dòng)畫(huà)幀都可以被用于運(yùn)行一次訓(xùn)練迭代。
class App extends Component {
...
componentDidMount () {
requestAnimationFrame(this.tick);
};
tick = () => {
this.setState((state) => ({
currentIteration: state.currentIteration + 1
}));
if (this.state.currentIteration < ITERATIONS) {
requestAnimationFrame(this.tick);
this.colorAccessibilityModel.train(this.state.currentIteration);
}
};
}
export default App;
此外,代價(jià)函數(shù)可以每 5 步進(jìn)行一次計(jì)算。如前所述,需要訪問(wèn) GPU 來(lái)檢索代價(jià)函數(shù)。因此需要防止神經(jīng)網(wǎng)絡(luò)訓(xùn)練過(guò)快。
class App extends Component {
...
componentDidMount () {
requestAnimationFrame(this.tick);
};
tick = () => {
this.setState((state) => ({
currentIteration: state.currentIteration + 1
}));
if (this.state.currentIteration < ITERATIONS) {
requestAnimationFrame(this.tick);
let computeCost = !(this.state.currentIteration % 5);
let cost = this.colorAccessibilityModel.train(
this.state.currentIteration,
computeCost
);
if (cost > 0) {
this.setState(() => ({ cost }));
}
}
};
}
export default App;
一旦組件裝載好訓(xùn)練階段就可以開(kāi)始運(yùn)行。現(xiàn)在是使用程序化計(jì)算輸出和預(yù)測(cè)輸出提供測(cè)試集的時(shí)候了。經(jīng)過(guò)時(shí)間推移,預(yù)測(cè)輸出應(yīng)該變得和程序化計(jì)算輸出一樣。而訓(xùn)練集本身并未被可視化。
class App extends Component {
...
render() {
const { currentIteration, cost } = this.state;
return (
Neural Network for Font Color Accessibility
Iterations: {currentIteration}
Cst: {cost}
/>
testSet={this.testSet}
/>
);
}
}
const ActualTable = ({ testSet }) =>
Programmatically Computed
const InferenceTable = ({ testSet, model }) =>
Neural Network Computed
export default App;
實(shí)際的表格會(huì)隨著測(cè)試集的不斷輸入不斷地展示每一個(gè)輸入和輸出的顏色。測(cè)試集包括輸入顏色(背景顏色)和輸出顏色(字體顏色)。由于生成數(shù)據(jù)集的時(shí)候輸出顏色被分類為黑色 [0,1] 和白色 [1,0] 向量,它們需要再次被轉(zhuǎn)換為真實(shí)的顏色。
const ActualTable = ({ testSet }) =>
Programmatically Computed
{Array(TEST_SET_SIZE).fill(0).map((v, i) =>rgbInput={testSet.rawInputs[i]}
rgbTarget={fromClassifierToRgb(testSet.rawTargets[i])}
/>
)}
const fromClassifierToRgb = (classifier) =>
classifier[0] > classifier[1]
? [ 255, 255, 255 ]
: [ 0, 0, 0 ]
ColorBox 組件是一個(gè)通用組件,以輸入顏色(背景顏色)和目標(biāo)顏色(字體顏色)為輸入。它能簡(jiǎn)單地用一個(gè)矩形展示輸入顏色的類型、輸入顏色的 RGB 代碼字符串,并用字體的 RGB 代碼將給定的目標(biāo)顏色上色。
const ColorBox = ({ rgbInput, rgbTarget }) =>
const RgbString = ({ rgb }) =>
`rgb(${rgb.toString()})`
const getRgbStyle = (rgb) =>
`rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`
最后但重要的是,在推理表格中可視化預(yù)測(cè)顏色的激動(dòng)人心的部分。它使用的也是 color box,但提供了一些不同的小道具。
const InferenceTable = ({ testSet, model }) =>
Neural Network Computed
{Array(TEST_SET_SIZE).fill(0).map((v, i) =>rgbInput={testSet.rawInputs[i]}
rgbTarget={fromClassifierToRgb(model.predict(testSet.rawInputs[i]))}
/>
)}
輸入顏色仍然是測(cè)試集中定義的顏色,但目標(biāo)顏色并不是測(cè)試集中的目標(biāo)色。任務(wù)的關(guān)鍵是利用神經(jīng)網(wǎng)絡(luò)的預(yù)測(cè)方法預(yù)測(cè)目標(biāo)顏色——它需要輸入的顏色,并應(yīng)在訓(xùn)練階段預(yù)測(cè)目標(biāo)顏色。
最后,當(dāng)你開(kāi)啟應(yīng)用時(shí),你需要觀察神經(jīng)網(wǎng)絡(luò)是否被啟用。而實(shí)際的表格從開(kāi)始就在使用固定測(cè)試集,在訓(xùn)練階段推理表格應(yīng)該改變它的字體顏色。事實(shí)上,當(dāng) ActualTable 組件顯示實(shí)際測(cè)試集時(shí),InferenceTable 顯示測(cè)試集的輸入數(shù)據(jù)點(diǎn),但輸出是使用神經(jīng)網(wǎng)絡(luò)預(yù)測(cè)的。
?
評(píng)論