近日,來自德國的 Robin Wieruch 發(fā)布了一系列使用 JavaScript 構(gòu)建機器學(xué)習(xí)的教程,本文將主要介紹使用 JavaScript 實現(xiàn)神經(jīng)網(wǎng)絡(luò)的方法。
JavaScript 是一種流行的高級編程語言,它被世界上的絕大多數(shù)網(wǎng)站所使用,也被所有主流瀏覽器所支持。隨著深度學(xué)習(xí)的火熱,越來越多開發(fā)者開始探索使用 JavaScript 實現(xiàn)人工智能與機器學(xué)習(xí)算法。近日,來自德國的 Robin Wieruch 發(fā)布了一系列使用 JavaScript 構(gòu)建機器學(xué)習(xí)的教程,本文將主要介紹使用 JavaScript 實現(xiàn)神經(jīng)網(wǎng)絡(luò)的方法。
近期,原作者發(fā)表了一系列有關(guān)在 JavaScript 上實現(xiàn)人工智能和機器學(xué)習(xí)算法的文章,其中包括:
線性回歸和梯度下降
正規(guī)方程線性回歸
邏輯回歸和梯度下降
這些機器學(xué)習(xí)算法的實現(xiàn)是基于 math.js 庫的線性代數(shù)(如矩陣運算)和微分的,你可以在 GitHub 上找到所有這些算法:
如果你發(fā)現(xiàn)其中存在任何缺陷,歡迎對這個資源提出自己的改進,以幫助后來者。我希望不斷為 web 開發(fā)者們提供更多、更豐富的機器學(xué)習(xí)算法。
就我個人來說,我發(fā)現(xiàn)實現(xiàn)這些算法在某種程度上是一個非常具有挑戰(zhàn)性的任務(wù)。特別是當(dāng)你需要在 JavaScript 上實現(xiàn)神經(jīng)網(wǎng)絡(luò)的前向和反向傳播的時候。由于我自己也在學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)的知識,我開始尋找適用于這種工作的庫。希望在不久的將來,我們能夠輕松地在 GitHub 上找到相關(guān)的基礎(chǔ)實現(xiàn)。然而現(xiàn)在,以我使用 JavaScript 的閱歷,我選擇了谷歌發(fā)布的 deeplearn.js 來進行此項工作。在本文中,我將分享使用 deeplearn.js 和 JavaScript 實現(xiàn)神經(jīng)網(wǎng)絡(luò)從而解決現(xiàn)實世界問題的方式——在 web 環(huán)境上。
首先,我強烈推薦讀者先學(xué)習(xí)一下深度學(xué)習(xí)著名學(xué)者吳恩達的《機器學(xué)習(xí)》課程。本文不會詳細解釋機器學(xué)習(xí)算法,只會展示它在 JavaScript 上的用法。另一方面,該系列課程在算法的細節(jié)和解釋上有著令人驚嘆的高質(zhì)量。在寫這篇文章之前,我自己也學(xué)習(xí)了相關(guān)課程,并試圖用 JavaScript 實現(xiàn)來內(nèi)化課程中的相關(guān)知識。
神經(jīng)網(wǎng)絡(luò)的目的是什么?
本文實現(xiàn)的神經(jīng)網(wǎng)絡(luò)需要通過選擇與背景顏色相關(guān)的適當(dāng)字體顏色來改善網(wǎng)頁可訪問性。比如,深藍色背景中的字體應(yīng)該是白色,而淺黃色背景中的字體應(yīng)該是黑色。你也許會想:首先你為什么需要一個神經(jīng)網(wǎng)絡(luò)來完成任務(wù)?通過編程的方式根據(jù)背景顏色計算可使用的字體顏色并不難,不是嗎?我很快在 Stack Overflow 找到了該問題的解決辦法,并根據(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)有一個編程的方法可以解決該問題的時候,使用神經(jīng)網(wǎng)絡(luò)對于該現(xiàn)實世界問題價值并不大,沒有必要使用一個機器訓(xùn)練的算法。然而,由于可通過編程解決這一問題,所以驗證神經(jīng)網(wǎng)絡(luò)的性能也變得很簡單,這也許能夠解決我們的問題。查看該 GitHub 庫(https://github.com/javascript-machine-learning/color-accessibility-neural-network-deeplearnjs)中的動圖,了解它最終表現(xiàn)如何,以及本教程中你將構(gòu)建什么。如果你熟悉機器學(xué)習(xí),也許你已經(jīng)注意到這個任務(wù)是一個分類問題。算法應(yīng)根據(jù)輸入(背景顏色)決定二進制輸出(字體顏色:白色或黑色)。在使用神經(jīng)網(wǎng)絡(luò)訓(xùn)練算法的過程中,最終會根據(jù)輸入的背景顏色輸出正確的字體顏色。
下文將從頭開始指導(dǎo)你設(shè)置神經(jīng)網(wǎng)絡(luò)的所有部分,并由你決定把文件/文件夾設(shè)置中的部分合在一起。但是你可以整合以前引用的 GitHub 庫以獲取實現(xiàn)細節(jié)。
JavaScript 中的數(shù)據(jù)集生成
? ? ? ? 機器學(xué)習(xí)中的訓(xùn)練集由輸入數(shù)據(jù)點和輸出數(shù)據(jù)點(標(biāo)簽)組成。它被用來訓(xùn)練為訓(xùn)練集(例如測試集)之外的新輸入數(shù)據(jù)點預(yù)測輸出的算法。在訓(xùn)練階段,由神經(jīng)網(wǎng)絡(luò)訓(xùn)練的算法調(diào)整其權(quán)重以預(yù)測輸入數(shù)據(jù)點的給定標(biāo)簽??傊?,已訓(xùn)練算法是一個以數(shù)據(jù)點作為輸入并近似輸出標(biāo)簽的函數(shù)。
? ? ? ?該算法經(jīng)過神經(jīng)網(wǎng)絡(luò)的訓(xùn)練后,可以為不屬于訓(xù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 的部分數(shù)據(jù)集。數(shù)據(jù)集中的數(shù)據(jù)點是 RGB 顏色空間中的顏色。每種顏色在矩陣中被表征為一行,而每一列是顏色的特征。特征是 RGB 空間中的 R、G、B 編碼值。數(shù)據(jù)集還沒有任何標(biāo)簽,所以訓(xùn)練集并不完整,因為它只有輸入值而沒有輸出值。
由于基于已知顏色生成可使用字體顏色的編程方法是已知的,因此可以使用調(diào)整后的功能版本以生成訓(xùn)練集(以及稍后的測試集)的標(biāo)簽。這些標(biāo)簽針對二分類問題進行了調(diào)整,并在 RGB 空間中隱含地反映了黑白的顏色。因此,對于黑色,標(biāo)簽是 [0,1];對于白色,標(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)備好一切用于生成(背景)顏色的隨機數(shù)據(jù)集(訓(xùn)練集、測試集),它被分類為黑色或白色(字體)顏色。
function generateColorSet(m) {
const rawInputs = generateRandomRgbColors(m);
const rawTargets = rawInputs.map(getAccessibleColor);
return { rawInputs, rawTargets };
}
使神經(jīng)網(wǎng)絡(luò)中底層算法更好的另一步操作是特征縮放。在特征縮放的簡化版本中,你希望 RGB 通道的值在 0 和 1 之間。由于你知道最大值,因此可以簡單地推導(dǎo)出每個顏色通道的歸一化值。
function normalizeColor(rgb) {
return rgb.map(v => v / 255);
}
你可以把這個功能放在你的神經(jīng)網(wǎng)絡(luò)模型中,或者作為單獨的效用函數(shù)。下一步我將把它放在神經(jīng)網(wǎng)絡(luò)模型中。
JavaScript 神經(jīng)網(wǎng)絡(luò)模型的設(shè)置階段
現(xiàn)在你可以使用 JavaScript 實現(xiàn)一個神經(jīng)網(wǎng)絡(luò)了。在開始之前,你需要先安裝 deeplearn.js 庫:一個適合 JavaScript 神經(jīng)網(wǎng)絡(luò)的框架。官方宣傳中說:「deeplearn.js 是一個開源庫,將高效的機器學(xué)習(xí)構(gòu)造塊帶到 web 中,允許在瀏覽器中訓(xùn)練神經(jīng)網(wǎng)絡(luò)或在推斷模式下運行預(yù)訓(xùn)練模型?!贡疚?,你將訓(xùn)練自己的模型,然后在推斷模式中運行該模型。使用該庫有兩個主要優(yōu)勢:
首先,它使用本地電腦的 GPU 加速機器學(xué)習(xí)算法中的向量計算。這些機器學(xué)習(xí)計算與圖解計算類似,因此使用 GPU 的計算比使用 CPU 更加高效。
其次,deeplearn.js 的結(jié)構(gòu)與流行的 TensorFlow 庫類似(TensorFlow 庫也是谷歌開發(fā)的,不過它使用的是 Python 語言)。因此如果你想在使用 Python 的機器學(xué)習(xí)中實現(xiàn)飛躍,那么 deeplearn.js 可提供通向 JavaScript 各領(lǐng)域的捷徑。
現(xiàn)在回到你的項目。如果你想用 npm 來設(shè)置,那么你只需要在命令行中安裝 deeplearn.js。也可以查看 deeplearn.js 項目的官方安裝說明文檔。
npm install deeplearn
我沒有構(gòu)建過大量神經(jīng)網(wǎng)絡(luò),因此我按照構(gòu)建神經(jīng)網(wǎng)絡(luò)的一般實踐進行操作。在 JavaScript 中,你可以使用 JavaScript ES6 class 來推進它。該類可以通過定義神經(jīng)網(wǎng)絡(luò)特性和類方法為你的神經(jīng)網(wǎng)絡(luò)提供完美的容器。例如,你的顏色歸一化函數(shù)可以在類別中找到一個作為方法的點。
class ColorAccessibilityModel {
normalizeColor(rgb) {
return rgb.map(v => v / 255);
}
}
export default ColorAccessibilityModel;
或許那也是你的函數(shù)生成數(shù)據(jù)集的地方。在我的案例中,我僅將類別歸一化作為分類方法,讓數(shù)據(jù)集生成獨立于類別之外。你可以認為未來有不同的方法來生成數(shù)據(jù)集,不應(yīng)該在神經(jīng)網(wǎng)絡(luò)模型中進行定義。不管怎樣,這只是一個實現(xiàn)細節(jié)。
訓(xùn)練和推斷階段都在機器學(xué)習(xí)的涵蓋性術(shù)語會話(session)之下。你可以在神經(jīng)網(wǎng)絡(luò)類別中設(shè)置會話。首先,你可以輸入來自 deeplearn.js 的 NDArrayMathGPU 類別,幫助你以計算高效的方式在 GPU 上進行數(shù)學(xué)運算。
import {
NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
...
}
export default ColorAccessibilityModel;
第二,聲明分類方法類設(shè)置會話。其函數(shù)簽名使用訓(xùn)練集作為參數(shù),成為從先前實現(xiàn)的函數(shù)中生成訓(xùn)練集的完美 consumer。
第三步,會話初始化空的圖。之后,圖將反映神經(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ù)點的形態(tài)。張量是具備不同維度的數(shù)組,它可以是向量、矩陣,或更高維度的矩陣。神經(jīng)網(wǎng)絡(luò)將這些張量作為輸入和輸出。在我們的案例中,有三個輸入單元(每個顏色通道有一個輸入單元)和兩個輸出單元(二分類,如黑白)。
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ò)提出自己的交叉計算參數(shù)(在會話中經(jīng)過訓(xù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,
) {
...
}
...
}
export default ColorAccessibilityModel;
根據(jù)層的數(shù)量,你可以變更圖來擴展出更多層。創(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ò)中的每一個神經(jīng)元必須具備一個定義好的激活函數(shù)。它可以是 logistic 激活函數(shù)。你或許已經(jīng)從 logistic 回歸中了解到它,它成為神經(jīng)網(wǎng)絡(luò)中的 logistic 單元。在我們的案例中,神經(jīng)網(wǎng)絡(luò)默認使用修正線性單元。
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)建輸出二分類的層。它有兩個輸出單元,每一個表示一個離散的值(黑色、白色)。
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;
第七步,聲明一個代價張量(cost tensor),以定義損失函數(shù)。在這個案例中,代價張量是均方誤差。它使用訓(xùn)練集的目標(biāo)張量(標(biāo)簽)和訓(xùn)練算法得到的預(yù)測張量來計算代價。
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)會話。之后,你就可以開始準(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;
不過目前在準(zhǔn)備神經(jīng)網(wǎng)絡(luò)的訓(xùn)練集之前,設(shè)置還沒完成。
首先,你可以在 GPU 數(shù)學(xué)計算環(huán)境中使用回調(diào)函數(shù)(callback function)來支持計算,但這并不是強制性的,可自主選擇。
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é)計算使用內(nèi)置的 NDArrays。你可以把它們理解為數(shù)組矩陣中的簡單數(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 的時候,deeplearn.js 提供的 shuffler 將二者保存在 sync 中。每次訓(xùn)練迭代都會出現(xiàn) shuffle,以饋送不同的輸入作為神經(jīng)網(wǎng)絡(luò)的 batch。整個 shuffle 流程可以改善訓(xùn)練算法,因為它更可能通過避免過擬合來實現(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ò)的所有層和單元都實現(xiàn)了,訓(xùn)練集也準(zhǔn)備好進行訓(xùn)練了?,F(xiàn)在只需要添加兩個配置神經(jīng)網(wǎng)絡(luò)行為的超參數(shù),它們適用于下個階段:訓(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;
第一個參數(shù)是學(xué)習(xí)速率(learning rate)。學(xué)習(xí)速率決定算法的收斂速度,以最小化成本。我們應(yīng)該假定它的數(shù)值很高,但實際上不能太高了。否則梯度下降就不會收斂,因為找不到局部最優(yōu)值。
第二個參數(shù)是批尺寸(batch size)。它定義每個 epoch(迭代)里有多少個訓(xùn)練集的數(shù)據(jù)點通過神經(jīng)網(wǎng)絡(luò)。一個 epoch 等于一批數(shù)據(jù)點的一次正向傳播和一次反向傳播。以批次的方式訓(xùn)練神經(jīng)網(wǎng)絡(luò)有兩個好處:第一,這樣可以防止密集計算,因為算法訓(xùn)練時使用了內(nèi)存中的少量數(shù)據(jù)點;第二,這樣可以讓神經(jīng)網(wǎng)絡(luò)更快地進行批處理,因為每個 epoch 中權(quán)重會隨著每個批次的數(shù)據(jù)點進行調(diào)整——而不是等到整個數(shù)據(jù)集訓(xùn)練完之后再進行改動。
訓(xùn)練階段
設(shè)置階段結(jié)束后就到了訓(xùn)練階段了。不需要太多實現(xiàn),因為所有的基礎(chǔ)都已在設(shè)置階段完成。首先,訓(xùn)練階段可以用分類方法來定義。然后在 deeplearn.js 的數(shù)學(xué)環(huán)境中再次執(zhí)行。此外,它還使用神經(jīng)網(wǎng)絡(luò)實例所有的預(yù)定義特性來訓(xùn)練算法。
class ColorAccessibilityModel {
...
train() {
math.scope(() => {
this.session.train(
this.costTensor,
this.feedEntries,
this.batchSize,
this.optimizer
);
});
}
}
export default ColorAccessibilityModel;
訓(xùn)練方法是 1 個 epoch 的神經(jīng)網(wǎng)絡(luò)訓(xùn)練。因此,從外部調(diào)用時,調(diào)用必須是迭代的。此外,訓(xùn)練只需要 1 個 epoch。為了多批次訓(xùn)練算法,你必須將該訓(xùn)練方法進行多次迭代運行。
這就是基礎(chǔ)的訓(xùn)練階段。但是根據(jù)時間調(diào)整學(xué)習(xí)率可以改善訓(xùn)練。學(xué)習(xí)率最初很高,但是當(dāng)算法在每一步過程中逐漸收斂時,學(xué)習(xí)率會出現(xiàn)下降趨勢。
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)練階段的損失,來驗證它是否隨著時間下降。損失可在每一次迭代時返回,不過這樣會導(dǎo)致較低的計算效率。神經(jīng)網(wǎng)絡(luò)每次請求返回損失,就必須通過 GPU 才能實現(xiàn)返回請求。因此,我們在多次迭代后僅要求返回一次損失來驗證其是否下降。如果沒有請求返回損失,則訓(xùn)練的損失下降常量被定義為 NONE(之前默認設(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)練階段。現(xiàn)在僅需要在訓(xùn)練集上進行會話設(shè)置后從外部進行迭代執(zhí)行。外部的執(zhí)行取決于訓(xùn)練方法是否返回損失。
推斷階段
最后一個階段是推斷階段,該階段使用測試集來驗證訓(xùn)練算法的性能。輸入是背景顏色中的 RGB 顏色,輸出是算法為字體顏色是黑是白進行的 [ 0, 1 ] 或 [ 1, 0 ] 分類預(yù)測。由于輸入數(shù)據(jù)點經(jīng)過歸一化,因此不要忘記在這一步也對顏色進行歸一化。
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)境中再次運行性能關(guān)鍵部分,需要定義一個映射,該映射最終可作為會話評估的輸入。記住,預(yù)測方法不是一定得在訓(xùn)練階段后運行。它可以在訓(xùn)練階段中使用,來輸出測試集的驗證。至此,神經(jīng)網(wǎng)絡(luò)已經(jīng)經(jīng)歷了設(shè)置、訓(xùn)練和推斷階段。
在 JavaScript 中可視化學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)
現(xiàn)在是時候使用神經(jīng)網(wǎng)絡(luò)進行訓(xùn)練和驗證/測試了。簡單的過程為建立一個神經(jīng)網(wǎng)絡(luò),使用一個訓(xùn)練集運行訓(xùn)練階段,代價函數(shù)取得最小值之后,使用一個測試集進行預(yù)測。所有的過程只需要使用網(wǎng)頁瀏覽器上的開發(fā)者控制臺的幾個 console.log statements 就可以完成。然而,由于該神經(jīng)網(wǎng)絡(luò)是關(guān)于顏色預(yù)測的,并且 deeplearn.js 是在瀏覽器上運行,從而可以輕松地對神經(jīng)網(wǎng)絡(luò)的訓(xùn)練階段和測試階段進行可視化。
至此,你可以自主決定你運行中的神經(jīng)網(wǎng)絡(luò)的可視化方式。使用一個 canvas 和 repuestAnimationFrame API 可以使 JavaScript 代碼更簡單。但就這篇文章來說,我會使用 React.js 進行展示,因為我在博客上寫過 React.js。
因此在使用 create-react-app 設(shè)置完項目后,App 組件可成為我們可視化的進入點。首先,導(dǎo)入神經(jīng)網(wǎng)絡(luò)類別和函數(shù),從你的文件中生成數(shù)據(jù)集。進而,為訓(xùn)練集大小、測試集大小和訓(xùn)練迭代次數(shù)添加若干個常量。
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)練集和測試集)、通過傳遞訓(xùn)練集建立神經(jīng)網(wǎng)絡(luò)會話、定義組件的初始狀態(tài)。在訓(xùn)練階段的時間內(nèi),代價函數(shù)的值和迭代次數(shù)會在控制臺上顯示,它也表示了組件的狀態(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;
接下來,設(shè)置了神經(jīng)網(wǎng)絡(luò)會話之后,就可以迭代地訓(xùn)練神經(jīng)網(wǎng)絡(luò)了。最簡單的版本只需要一直運行 React 的一個 for 循環(huán)就可以了。
class App extends Component {
...
componentDidMount () {
for (let i = 0; i <= ITERATIONS; i++) {
this.colorAccessibilityModel.train(i);
}
};
}
export default App;
然而,以上代碼不會在 React 的訓(xùn)練階段提供(render)輸出,因為組件不會在神經(jīng)網(wǎng)絡(luò)阻塞單個 JavaScript 線程的時候 reRender。這也正是 React 使用 requestAnimationFrame 的時候。與其自己定義一個 for 循環(huá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;
此外,代價函數(shù)可以每 5 步進行一次計算。如前所述,需要訪問 GPU 來檢索代價函數(shù)。因此需要防止神經(jīng)網(wǎng)絡(luò)訓(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);
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)練階段就可以開始運行?,F(xiàn)在是使用程序化計算輸出和預(yù)測輸出提供測試集的時候了。經(jīng)過時間推移,預(yù)測輸出應(yīng)該變得和程序化計算輸出一樣。而訓(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ù)據(jù)集的時候輸出顏色被分類為黑色 [0,1] 和白色 [1,0] 向量,它們需要再次被轉(zhuǎn)換為真實的顏色。
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 組件是一個通用組件,以輸入顏色(背景顏色)和目標(biāo)顏色(字體顏色)為輸入。它能簡單地用一個矩形展示輸入顏色的類型、輸入顏色的 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ù)測顏色的激動人心的部分。它使用的也是 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]))}
/>
)}
輸入顏色仍然是測試集中定義的顏色,但目標(biāo)顏色并不是測試集中的目標(biāo)色。任務(wù)的關(guān)鍵是利用神經(jīng)網(wǎng)絡(luò)的預(yù)測方法預(yù)測目標(biāo)顏色——它需要輸入的顏色,并應(yīng)在訓(xùn)練階段預(yù)測目標(biāo)顏色。
最后,當(dāng)你開啟應(yīng)用時,你需要觀察神經(jīng)網(wǎng)絡(luò)是否被啟用。而實際的表格從開始就在使用固定測試集,在訓(xùn)練階段推理表格應(yīng)該改變它的字體顏色。事實上,當(dāng) ActualTable 組件顯示實際測試集時,InferenceTable 顯示測試集的輸入數(shù)據(jù)點,但輸出是使用神經(jīng)網(wǎng)絡(luò)預(yù)測的。
?
評論
查看更多