聽說你用JavaScript寫程式碼?本文是你的機器學習指南

機器之心發表於2017-12-06

聽說你用JavaScript寫程式碼?本文是你的機器學習指南


有網友對此表示:「我本想寫一篇激烈的反駁文,其中闡述如果沒有 GPU 的支援,這種做法是毫無意義的……但它可以使用 WebGL 來應用 GPU 的能力。而且,這可能比你在本地桌面上安裝 TensorFlow 堆疊要簡單一萬倍。」

近期,原作者發表了一系列有關在 JavaScript 上實現人工智慧和機器學習演算法的文章,其中包括:

這些機器學習演算法的實現是基於 math.js 庫的線性代數(如矩陣運算)和微分的,你可以在 GitHub 上找到所有這些演算法:

GitHub 連結:https://github.com/javascript-machine-learning

如果你發現其中存在任何缺陷,歡迎對這個資源提出自己的改進,以幫助後來者。我希望不斷為 web 開發者們提供更多、更豐富的機器學習演算法。

就我個人來說,我發現實現這些演算法在某種程度上是一個非常具有挑戰性的任務。特別是當你需要在 JavaScript 上實現神經網路的前向和反向傳播的時候。由於我自己也在學習神經網路的知識,我開始尋找適用於這種工作的庫。希望在不久的將來,我們能夠輕鬆地在 GitHub 上找到相關的基礎實現。然而現在,以我使用 JavaScript 的閱歷,我選擇了谷歌釋出的 deeplearn.js 來進行此項工作。在本文中,我將分享使用 deeplearn.js 和 JavaScript 實現神經網路從而解決現實世界問題的方式——在 web 環境上。

首先,我強烈推薦讀者先學習一下深度學習著名學者吳恩達的《機器學習》課程。本文不會詳細解釋機器學習演算法,只會展示它在 JavaScript 上的用法。另一方面,該系列課程在演算法的細節和解釋上有著令人驚歎的高質量。在寫這篇文章之前,我自己也學習了相關課程,並試圖用 JavaScript 實現來內化課程中的相關知識。

神經網路的目的是什麼?

本文實現的神經網路需要通過選擇與背景顏色相關的適當字型顏色來改善網頁可訪問性。比如,深藍色背景中的字型應該是白色,而淺黃色背景中的字型應該是黑色。你也許會想:首先你為什麼需要一個神經網路來完成任務?通過程式設計的方式根據背景顏色計算可使用的字型顏色並不難,不是嗎?我很快在 Stack Overflow 找到了該問題的解決辦法,並根據我的需求做了調整,以適應 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 ];
}

當已經有一個程式設計的方法可以解決該問題的時候,使用神經網路對於該現實世界問題價值並不大,沒有必要使用一個機器訓練的演算法。然而,由於可通過程式設計解決這一問題,所以驗證神經網路的效能也變得很簡單,這也許能夠解決我們的問題。檢視該 GitHub 庫(https://github.com/javascript-machine-learning/color-accessibility-neural-network-deeplearnjs)中的動圖,瞭解它最終表現如何,以及本教程中你將構建什麼。如果你熟悉機器學習,也許你已經注意到這個任務是一個分類問題。演算法應根據輸入(背景顏色)決定二進位制輸出(字型顏色:白色或黑色)。在使用神經網路訓練演算法的過程中,最終會根據輸入的背景顏色輸出正確的字型顏色。

下文將從頭開始指導你設定神經網路的所有部分,並由你決定把檔案/資料夾設定中的部分合在一起。但是你可以整合以前引用的 GitHub 庫以獲取實現細節。

JavaScript 中的資料集生成

機器學習中的訓練集由輸入資料點和輸出資料點(標籤)組成。它被用來訓練為訓練集(例如測試集)之外的新輸入資料點預測輸出的演算法。在訓練階段,由神經網路訓練的演算法調整其權重以預測輸入資料點的給定標籤。總之,已訓練演算法是一個以資料點作為輸入並近似輸出標籤的函式。

該演算法經過神經網路的訓練後,可以為不屬於訓練集的新背景顏色輸出字型顏色。因此,稍後你將使用測試集來驗證訓練演算法的準確率。由於我們正在處理顏色,因此為神經網路生成輸入顏色的樣本資料集並不困難。

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() 函式建立給定大小為 m 的部分資料集。資料集中的資料點是 RGB 顏色空間中的顏色。每種顏色在矩陣中被表徵為一行,而每一列是顏色的特徵。特徵是 RGB 空間中的 R、G、B 編碼值。資料集還沒有任何標籤,所以訓練集並不完整,因為它只有輸入值而沒有輸出值。

由於基於已知顏色生成可使用字型顏色的程式設計方法是已知的,因此可以使用調整後的功能版本以生成訓練集(以及稍後的測試集)的標籤。這些標籤針對二分類問題進行了調整,並在 RGB 空間中隱含地反映了黑白的顏色。因此,對於黑色,標籤是 [0,1];對於白色,標籤是 [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
}

現在你已經準備好一切用於生成(背景)顏色的隨機資料集(訓練集、測試集),它被分類為黑色或白色(字型)顏色。

function generateColorSet(m) {
 const rawInputs = generateRandomRgbColors(m);
 const rawTargets = rawInputs.map(getAccessibleColor);
 return { rawInputs, rawTargets };
}

使神經網路中底層演算法更好的另一步操作是特徵縮放。在特徵縮放的簡化版本中,你希望 RGB 通道的值在 0 和 1 之間。由於你知道最大值,因此可以簡單地推匯出每個顏色通道的歸一化值。

function normalizeColor(rgb) {
 return rgb.map(v => v / 255);
}

你可以把這個功能放在你的神經網路模型中,或者作為單獨的效用函式。下一步我將把它放在神經網路模型中。

JavaScript 神經網路模型的設定階段

現在你可以使用 JavaScript 實現一個神經網路了。在開始之前,你需要先安裝 deeplearn.js 庫:一個適合 JavaScript 神經網路的框架。官方宣傳中說:「deeplearn.js 是一個開源庫,將高效的機器學習構造塊帶到 web 中,允許在瀏覽器中訓練神經網路或在推斷模式下執行預訓練模型。」本文,你將訓練自己的模型,然後在推斷模式中執行該模型。使用該庫有兩個主要優勢:

  • 首先,它使用本地電腦的 GPU 加速機器學習演算法中的向量計算。這些機器學習計算與圖解計算類似,因此使用 GPU 的計算比使用 CPU 更加高效。
  • 其次,deeplearn.js 的結構與流行的 TensorFlow 庫類似(TensorFlow 庫也是谷歌開發的,不過它使用的是 Python 語言)。因此如果你想在使用 Python 的機器學習中實現飛躍,那麼 deeplearn.js 可提供通向 JavaScript 各領域的捷徑。

現在回到你的專案。如果你想用 npm 來設定,那麼你只需要在命令列中安裝 deeplearn.js。也可以檢視 deeplearn.js 專案的官方安裝說明文件。

npm install deeplearn

我沒有構建過大量神經網路,因此我按照構建神經網路的一般實踐進行操作。在 JavaScript 中,你可以使用 JavaScript ES6 class 來推進它。該類可以通過定義神經網路特性和類方法為你的神經網路提供完美的容器。例如,你的顏色歸一化函式可以在類別中找到一個作為方法的點。

class ColorAccessibilityModel {
 normalizeColor(rgb) {
   return rgb.map(v => v / 255);
 }
}
export default ColorAccessibilityModel;

或許那也是你的函式生成資料集的地方。在我的案例中,我僅將類別歸一化作為分類方法,讓資料集生成獨立於類別之外。你可以認為未來有不同的方法來生成資料集,不應該在神經網路模型中進行定義。不管怎樣,這只是一個實現細節。

訓練和推斷階段都在機器學習的涵蓋性術語會話(session)之下。你可以在神經網路類別中設定會話。首先,你可以輸入來自 deeplearn.js 的 NDArrayMathGPU 類別,幫助你以計算高效的方式在 GPU 上進行數學運算。

import {
 NDArrayMathGPU,
} from 'deeplearn';
const math = new NDArrayMathGPU();
class ColorAccessibilityModel {
 ...
}
export default ColorAccessibilityModel;

第二,宣告分類方法類設定會話。其函式簽名使用訓練集作為引數,成為從先前實現的函式中生成訓練集的完美 consumer。第三步,會話初始化空的圖。之後,圖將反映神經網路的架構。你可以隨意定義其特性。

import {
 Graph,
 NDArrayMathGPU,
} from 'deeplearn';
class ColorAccessibilityModel {
 setupSession(trainingSet) {
   const graph = new Graph();
 }
 ..
}
export default ColorAccessibilityModel;

第四步,你用張量的形式定義圖中輸入和輸出資料點的形態。張量是具備不同維度的陣列,它可以是向量、矩陣,或更高維度的矩陣。神經網路將這些張量作為輸入和輸出。在我們的案例中,有三個輸入單元(每個顏色通道有一個輸入單元)和兩個輸出單元(二分類,如黑白)。

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;

第五步,神經網路包含隱藏層。奇蹟如何發生目前仍是黑箱。基本上,神經網路提出自己的交叉計算引數(在會話中經過訓練)。不過,你可以隨意定義隱藏層的維度(每個單元大小、層大小)。

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;

根據層的數量,你可以變更圖來擴充套件出更多層。建立連線層的分類方法需要圖、變異連線層(mutated connected layer)、新層的索引,以及單元數量。圖的層屬性可用於返回由名稱確定的新張量。

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;

神經網路中的每一個神經元必須具備一個定義好的啟用函式。它可以是 logistic 啟用函式。你或許已經從 logistic 迴歸中瞭解到它,它成為神經網路中的 logistic 單元。在我們的案例中,神經網路預設使用修正線性單元。

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;

第六步,建立輸出二分類的層。它有兩個輸出單元,每一個表示一個離散的值(黑色、白色)。

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),以定義損失函式。在這個案例中,代價張量是均方誤差。它使用訓練集的目標張量(標籤)和訓練演算法得到的預測張量來計算代價。

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;

最後但並非不重要的一步,設定架構圖的相關會話。之後,你就可以開始準備為訓練階段匯入訓練集了。

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;

不過目前在準備神經網路的訓練集之前,設定還沒完成。首先,你可以在 GPU 數學計算環境中使用回撥函式(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;

其次,你可以解構訓練集的輸入和輸出(標籤,也稱為目標)以將其轉換成神經網路可讀的格式。deeplearn.js 的數學計算使用內建的 NDArrays。你可以把它們理解為陣列矩陣中的簡單陣列或向量。此外,輸入陣列的顏色被歸一化以提高神經網路的效能。

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 輸入和目標陣列。shuffle 的時候,deeplearn.js 提供的 shuffler 將二者儲存在 sync 中。每次訓練迭代都會出現 shuffle,以饋送不同的輸入作為神經網路的 batch。整個 shuffle 流程可以改善訓練演算法,因為它更可能通過避免過擬合來實現泛化。

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)是訓練階段中神經網路前饋演算法的最終輸入。它匹配資料和張量(根據設定階段的形態而定義)。

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;

這樣,神經網路的設定就結束了。神經網路的所有層和單元都實現了,訓練集也準備好進行訓練了。現在只需要新增兩個配置神經網路行為的超引數,它們適用於下個階段:訓練階段。

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;

第一個引數是學習速率(learning rate)。學習速率決定演算法的收斂速度,以最小化成本。我們應該假定它的數值很高,但實際上不能太高了。否則梯度下降就不會收斂,因為找不到區域性最優值。

第二個引數是批尺寸(batch size)。它定義每個 epoch(迭代)裡有多少個訓練集的資料點通過神經網路。一個 epoch 等於一批資料點的一次正向傳播和一次反向傳播。以批次的方式訓練神經網路有兩個好處:第一,這樣可以防止密集計算,因為演算法訓練時使用了記憶體中的少量資料點;第二,這樣可以讓神經網路更快地進行批處理,因為每個 epoch 中權重會隨著每個批次的資料點進行調整——而不是等到整個資料集訓練完之後再進行改動。

訓練階段

設定階段結束後就到了訓練階段了。不需要太多實現,因為所有的基礎都已在設定階段完成。首先,訓練階段可以用分類方法來定義。然後在 deeplearn.js 的數學環境中再次執行。此外,它還使用神經網路例項所有的預定義特性來訓練演算法。

class ColorAccessibilityModel {
 ...
 train() {
   math.scope(() => {
     this.session.train(
       this.costTensor,
       this.feedEntries,
       this.batchSize,
       this.optimizer
     );
   });
 }
}
export default ColorAccessibilityModel;

訓練方法是 1 個 epoch 的神經網路訓練。因此,從外部呼叫時,呼叫必須是迭代的。此外,訓練只需要 1 個 epoch。為了多批次訓練演算法,你必須將該訓練方法進行多次迭代執行。

這就是基礎的訓練階段。但是根據時間調整學習率可以改善訓練。學習率最初很高,但是當演算法在每一步過程中逐漸收斂時,學習率會出現下降趨勢。

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;

在我們的情況中,學習率每 50 步下降 10%。下面,我們需要獲取訓練階段的損失,來驗證它是否隨著時間下降。損失可在每一次迭代時返回,不過這樣會導致較低的計算效率。神經網路每次請求返回損失,就必須通過 GPU 才能實現返回請求。因此,我們在多次迭代後僅要求返回一次損失來驗證其是否下降。如果沒有請求返回損失,則訓練的損失下降常量被定義為 NONE(之前預設設定)。

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;

最後,這就是訓練階段。現在僅需要在訓練集上進行會話設定後從外部進行迭代執行。外部的執行取決於訓練方法是否返回損失。

推斷階段

最後一個階段是推斷階段,該階段使用測試集來驗證訓練演算法的效能。輸入是背景顏色中的 RGB 顏色,輸出是演算法為字型顏色是黑是白進行的 [ 0, 1 ] 或 [ 1, 0 ] 分類預測。由於輸入資料點經過歸一化,因此不要忘記在這一步也對顏色進行歸一化。

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;

該方法在數學環境中再次執行效能關鍵部分,需要定義一個對映,該對映最終可作為會話評估的輸入。記住,預測方法不是一定得在訓練階段後執行。它可以在訓練階段中使用,來輸出測試集的驗證。至此,神經網路已經經歷了設定、訓練和推斷階段。


在 JavaScript 中視覺化學習神經網路

現在是時候使用神經網路進行訓練和驗證/測試了。簡單的過程為建立一個神經網路,使用一個訓練集執行訓練階段,代價函式取得最小值之後,使用一個測試集進行預測。所有的過程只需要使用網頁瀏覽器上的開發者控制檯的幾個 console.log statements 就可以完成。然而,由於該神經網路是關於顏色預測的,並且 deeplearn.js 是在瀏覽器上執行,從而可以輕鬆地對神經網路的訓練階段和測試階段進行視覺化。

至此,你可以自主決定你執行中的神經網路的視覺化方式。使用一個 canvas 和 repuestAnimationFrame API 可以使 JavaScript 程式碼更簡單。但就這篇文章來說,我會使用 React.js 進行展示,因為我在部落格上寫過 React.js。

因此在使用 create-react-app 設定完專案後,App 元件可成為我們視覺化的進入點。首先,匯入神經網路類別和函式,從你的檔案中生成資料集。進而,為訓練集大小、測試集大小和訓練迭代次數新增若干個常量。

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 的元件包括生成資料集(訓練集和測試集)、通過傳遞訓練集建立神經網路會話、定義元件的初始狀態。在訓練階段的時間內,代價函式的值和迭代次數會在控制檯上顯示,它也表示了元件的狀態。

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;

接下來,設定了神經網路會話之後,就可以迭代地訓練神經網路了。最簡單的版本只需要一直執行 React 的一個 for 迴圈就可以了。

class App extends Component {
 ...
 componentDidMount () {
   for (let i = 0; i <= ITERATIONS; i++) {
     this.colorAccessibilityModel.train(i);
   }
 };
}
export default App;

然而,以上程式碼不會在 React 的訓練階段提供(render)輸出,因為元件不會在神經網路阻塞單個 JavaScript 執行緒的時候 reRender。這也正是 React 使用 requestAnimationFrame 的時候。與其自己定義一個 for 迴圈,每一個請求的瀏覽器的動畫幀都可以被用於執行一次訓練迭代。

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;

此外,代價函式可以每 5 步進行一次計算。如前所述,需要訪問 GPU 來檢索代價函式。因此需要防止神經網路訓練過快。

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;

一旦元件裝載好訓練階段就可以開始執行。現在是使用程式化計算輸出和預測輸出提供測試集的時候了。經過時間推移,預測輸出應該變得和程式化計算輸出一樣。而訓練集本身並未被視覺化。

class App extends Component {
 ...
 render() {
   const { currentIteration, cost } = this.state;
   return (
     <div className="app">
       <div>
         <h1>Neural Network for Font Color Accessibility</h1>
         <p>Iterations: {currentIteration}</p>
         <p>Cost: {cost}</p>
       </div>
       <div className="content">
         <div className="content-item">
           <ActualTable
             testSet={this.testSet}
           />
         </div>
         <div className="content-item">
           <InferenceTable
             model={this.colorAccessibilityModel}
             testSet={this.testSet}
           />
         </div>
       </div>
     </div>
   );
 }
}
const ActualTable = ({ testSet }) =>
 <div>
   <p>Programmatically Computed</p>
 </div>
const InferenceTable = ({ testSet, model }) =>
 <div>
   <p>Neural Network Computed</p>
 </div>
export default App;

實際的表格會隨著測試集的不斷輸入不斷地展示每一個輸入和輸出的顏色。測試集包括輸入顏色(背景顏色)和輸出顏色(字型顏色)。由於生成資料集的時候輸出顏色被分類為黑色 [0,1] 和白色 [1,0] 向量,它們需要再次被轉換為真實的顏色。

const ActualTable = ({ testSet }) =>
 <div>
   <p>Programmatically Computed</p>
   {Array(TEST_SET_SIZE).fill(0).map((v, i) =>
     <ColorBox
       key={i}
       rgbInput={testSet.rawInputs[i]}
       rgbTarget={fromClassifierToRgb(testSet.rawTargets[i])}
     />
   )}
 </div>
const fromClassifierToRgb = (classifier) =>
 classifier[0] > classifier[1]
   ? [ 255, 255, 255 ]
   : [ 0, 0, 0 ]

ColorBox 元件是一個通用元件,以輸入顏色(背景顏色)和目標顏色(字型顏色)為輸入。它能簡單地用一個矩形展示輸入顏色的型別、輸入顏色的 RGB 程式碼字串,並用字型的 RGB 程式碼將給定的目標顏色上色。

const ColorBox = ({ rgbInput, rgbTarget }) =>
 <div className="color-box" style={{ backgroundColor: getRgbStyle(rgbInput) }}>
   <span style={{ color: getRgbStyle(rgbTarget) }}>
     <RgbString rgb={rgbInput} />
   </span>
 </div>
const RgbString = ({ rgb }) =>
 `rgb(${rgb.toString()})`
const getRgbStyle = (rgb) =>
 `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`

最後但重要的是,在推理表格中視覺化預測顏色的激動人心的部分。它使用的也是 color box,但提供了一些不同的小道具。

const InferenceTable = ({ testSet, model }) =>
 <div>
   <p>Neural Network Computed</p>
   {Array(TEST_SET_SIZE).fill(0).map((v, i) =>
     <ColorBox
       key={i}
       rgbInput={testSet.rawInputs[i]}
       rgbTarget={fromClassifierToRgb(model.predict(testSet.rawInputs[i]))}
     />
   )}
 </div>

輸入顏色仍然是測試集中定義的顏色,但目標顏色並不是測試集中的目標色。任務的關鍵是利用神經網路的預測方法預測目標顏色——它需要輸入的顏色,並應在訓練階段預測目標顏色。

最後,當你開啟應用時,你需要觀察神經網路是否被啟用。而實際的表格從開始就在使用固定測試集,在訓練階段推理表格應該改變它的字型顏色。事實上,當 ActualTable 元件顯示實際測試集時,InferenceTable 顯示測試集的輸入資料點,但輸出是使用神經網路預測的。

本文向你展示瞭如何使用 deeplearn.js 在 JavaScript 上為機器學習構建神經網路,希望對大家有所幫助。如果你有任何改進的建議,歡迎留言並在 GitHub 上做出自己的貢獻。React 渲染部分的視覺化動圖可以在 GitHub 上看到:https://github.com/javascript-machine-learning/color-accessibility-neural-network-deeplearnjs

相關文章