TensorFlow.js - 透過 CNN 使用手寫數字辨識功能

1. 簡介

在本教學課程中,我們將建構 TensorFlow.js 模型,透過卷積類神經網路辨識手寫數字。首先,我們會讓分類器「查看」數千張手寫數字圖片及其標籤,藉此訓練分類器。接著,我們會使用模型從未見過的測試資料,評估分類器的準確率。

這項工作視為分類工作,因為我們訓練模型是為了將類別 (圖片中顯示的數字) 指派給輸入圖片。我們會向模型展示許多輸入內容和正確輸出內容的範例,藉此訓練模型。這就是所謂的監督式學習

建構目標

您將建立網頁,使用 TensorFlow.js 在瀏覽器中訓練模型。只要提供特定大小的黑白圖片,系統就會分類圖片中顯示的數字。步驟如下:

  • 載入資料。
  • 定義模型架構。
  • 訓練模型並監控訓練期間的效能。
  • 進行一些預測,評估訓練好的模型。

課程內容

  • 使用 TensorFlow.js Layers API 建立卷積模型的 TensorFlow.js 語法。
  • 在 TensorFlow.js 中制定分類工作
  • 如何使用 tfjs-vis 程式庫監控瀏覽器內訓練。

軟硬體需求

您也應熟悉第一堂訓練課程的內容。

2. 做好準備

建立 HTML 網頁並加入 JavaScript

96914ff65fc3b74c.png將下列程式碼複製到名為

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TensorFlow.js Tutorial</title>

  <!-- Import TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
  <!-- Import tfjs-vis -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>

  <!-- Import the data file -->
  <script src="data.js" type="module"></script>

  <!-- Import the main script file -->
  <script src="script.js" type="module"></script>

</head>

<body>
</body>
</html>

建立資料和程式碼的 JavaScript 檔案

  1. 在與上述 HTML 檔案相同的資料夾中,建立名為 data.js 的檔案,然後將這個連結中的內容複製到該檔案。
  2. 在與步驟一相同的資料夾中,建立名為 script.js 的檔案,並在其中加入下列程式碼。
console.log('Hello TensorFlow');

立即測試

現在您已建立 HTML 和 JavaScript 檔案,請測試這些檔案。在瀏覽器中開啟 index.html 檔案,並開啟開發人員工具控制台。

如果一切正常,應該會建立兩個全域變數。tf 是指 TensorFlow.js 程式庫,tfvis 則是指 tfjs-vis 程式庫

畫面上應會顯示「Hello TensorFlow訊息,如果看到這則訊息,即可繼續下一個步驟。

3. 載入資料

在本教學課程中,您將訓練模型,學習辨識圖片中的數字,如下所示。這些圖片是來自 MNIST 資料集的 28x28 像素灰階圖片。

mnist 4 mnist 3 mnist 8

我們已提供程式碼,可從我們為您建立的特殊 sprite 檔案 (~10MB) 載入這些圖片,方便您專注於訓練部分。

歡迎研究 data.js 檔案,瞭解資料的載入方式。或者,完成本教學課程後,您也可以自行建立載入資料的方法。

提供的程式碼包含一個類別 MnistData,其中有兩個公開方法:

  • nextTrainBatch(batchSize):從訓練集傳回隨機批次的圖片和標籤。
  • nextTestBatch(batchSize):從測試集傳回一批圖片及其標籤

MnistData 類別也會執行隨機排序正規化資料的重要步驟。

共有 65,000 張圖片,我們將使用最多 55,000 張圖片訓練模型,並保留 10,000 張圖片,以便在完成後測試模型效能。我們會在瀏覽器中完成所有操作!

現在載入資料,並測試是否正確載入。

96914ff65fc3b74c.png 將下列程式碼新增至 script.js 檔案。

import {MnistData} from './data.js';

async function showExamples(data) {
  // Create a container in the visor
  const surface =
    tfvis.visor().surface({ name: 'Input Data Examples', tab: 'Input Data'});  

  // Get the examples
  const examples = data.nextTestBatch(20);
  const numExamples = examples.xs.shape[0];
  
  // Create a canvas element to render each example
  for (let i = 0; i < numExamples; i++) {
    const imageTensor = tf.tidy(() => {
      // Reshape the image to 28x28 px
      return examples.xs
        .slice([i, 0], [1, examples.xs.shape[1]])
        .reshape([28, 28, 1]);
    });
    
    const canvas = document.createElement('canvas');
    canvas.width = 28;
    canvas.height = 28;
    canvas.style = 'margin: 4px;';
    await tf.browser.toPixels(imageTensor, canvas);
    surface.drawArea.appendChild(canvas);

    imageTensor.dispose();
  }
}

async function run() {  
  const data = new MnistData();
  await data.load();
  await showExamples(data);
}

document.addEventListener('DOMContentLoaded', run);

重新整理頁面,幾秒後左側應該會顯示面板,內含多張圖片。

6dff857738b54eed.png

4. 構思工作內容

輸入資料如下所示。

6dff857738b54eed.png

我們的目標是訓練模型,讓模型接收一張圖片,並學習預測該圖片可能屬於的 10 個類別 (數字 0 到 9) 各自的分數

每張圖片的寬度和高度都是 28 像素,且由於是灰階圖片,因此只有 1 個顏色通道。因此每張圖片的形狀都是 [28, 28, 1]

請注意,我們進行的是一對十的對應,以及每個輸入範例的形狀,因為這對下一節來說很重要。

5. 定義模型架構

在本節中,我們將編寫程式碼來描述模型架構。模型架構是比較複雜的說法,意指「模型執行時會執行的函式」,或「模型會使用哪種演算法計算答案」。

在機器學習中,我們會定義架構 (或演算法),並讓訓練程序學習該演算法的參數。

96914ff65fc3b74c.png 中新增下列函式

script.js 檔案,用於定義模型架構

function getModel() {
  const model = tf.sequential();
  
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const IMAGE_CHANNELS = 1;  
  
  // In the first layer of our convolutional neural network we have 
  // to specify the input shape. Then we specify some parameters for 
  // the convolution operation that takes place in this layer.
  model.add(tf.layers.conv2d({
    inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
    kernelSize: 5,
    filters: 8,
    strides: 1,
    activation: 'relu',
    kernelInitializer: 'varianceScaling'
  }));

  // The MaxPooling layer acts as a sort of downsampling using max values
  // in a region instead of averaging.  
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
  
  // Repeat another conv2d + maxPooling stack. 
  // Note that we have more filters in the convolution.
  model.add(tf.layers.conv2d({
    kernelSize: 5,
    filters: 16,
    strides: 1,
    activation: 'relu',
    kernelInitializer: 'varianceScaling'
  }));
  model.add(tf.layers.maxPooling2d({poolSize: [2, 2], strides: [2, 2]}));
  
  // Now we flatten the output from the 2D filters into a 1D vector to prepare
  // it for input into our last layer. This is common practice when feeding
  // higher dimensional data to a final classification output layer.
  model.add(tf.layers.flatten());

  // Our last layer is a dense layer which has 10 output units, one for each
  // output class (i.e. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9).
  const NUM_OUTPUT_CLASSES = 10;
  model.add(tf.layers.dense({
    units: NUM_OUTPUT_CLASSES,
    kernelInitializer: 'varianceScaling',
    activation: 'softmax'
  }));

  
  // Choose an optimizer, loss function and accuracy metric,
  // then compile and return the model
  const optimizer = tf.train.adam();
  model.compile({
    optimizer: optimizer,
    loss: 'categoricalCrossentropy',
    metrics: ['accuracy'],
  });

  return model;
}

讓我們進一步瞭解這項功能。

卷積

model.add(tf.layers.conv2d({
  inputShape: [IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_CHANNELS],
  kernelSize: 5,
  filters: 8,
  strides: 1,
  activation: 'relu',
  kernelInitializer: 'varianceScaling'
}));

這裡我們使用的是序列模型。

我們使用的是 conv2d 層,而非稠密層。我們無法詳細說明捲積的運作方式,但以下資源會說明基礎作業:

讓我們來細看 conv2d 設定物件中的每個引數:

  • inputShape。資料的形狀,會流入模型的第一層。在本例中,MNIST 範例是 28x28 像素的黑白圖片。圖片資料的標準格式為 [row, column, depth],因此我們要在這裡設定 [28, 28, 1] 的形狀。每個維度的像素數為 28 列和 28 欄,深度為 1,因為圖片只有 1 個顏色通道。請注意,我們不會在輸入形狀中指定批次大小。層的設計與批次大小無關,因此在推論期間,您可以傳遞任何批次大小的張量。
  • kernelSize. 要套用至輸入資料的滑動捲積篩選器視窗大小。這裡我們將 kernelSize 設為 5,指定 5x5 的方形捲積視窗。
  • filters:要套用至輸入資料的 kernelSize 大小篩選器視窗數量。我們將對資料套用 8 個篩選器。
  • strides。滑動視窗的「步長」,也就是篩選器每次在圖片上移動時會位移的像素數。這裡我們指定步幅為 1,也就是說,篩選器會以 1 像素的步幅滑過圖片。
  • activation。這是指在完成捲積後套用至資料的啟動函式。在本例中,我們套用了線性整流函數 (ReLU),這是機器學習模型中非常常見的啟動函數。
  • kernelInitializer。用於隨機初始化模型權重的方法,對訓練動態非常重要。我們不會在此詳述初始化作業,但 VarianceScaling (在此使用) 一般是初始化工具的合適選擇

簡化資料表示法

model.add(tf.layers.flatten());

圖片是高維度資料,而捲積運算往往會增加輸入資料的大小。在將資料傳遞至最終分類層之前,我們需要將資料攤平為一個長陣列。密集層 (我們用做最後一層) 只需 tensor1d 秒,因此這個步驟在許多分類工作中都很常見。

計算最終機率分配

const NUM_OUTPUT_CLASSES = 10;
model.add(tf.layers.dense({
  units: NUM_OUTPUT_CLASSES,
  kernelInitializer: 'varianceScaling',
  activation: 'softmax'
}));

我們會使用 softmax 啟動的密集層,計算 10 個可能類別的機率分布。分數最高的類別就是預測的數字。

選擇最佳化工具和損失函式

const optimizer = tf.train.adam();
model.compile({
  optimizer: optimizer,
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});

我們會編譯模型,指定 最佳化工具損失函式和要追蹤的指標。

與第一個教學課程不同,這裡我們使用 categoricalCrossentropy 做為損失函式。顧名思義,當模型的輸出結果為機率分布時,就會使用這個函式。categoricalCrossentropy 會測量模型最後一層產生的機率分布,與真實標籤提供的機率分布之間的誤差。

舉例來說,如果我們的數字確實代表 7,可能會得到下列結果

索引

0

1

2

3

4

5

6

7

8

9

True 標籤

0

0

0

0

0

0

0

1

0

0

預測

0.1

0.01

0.01

0.01

0.20

0.01

0.01

0.60

0.03

0.02

類別交叉熵會產生單一數字,指出預測向量與真實標籤向量的相似程度。

這裡使用的標籤資料表示法稱為「one-hot 編碼」,常見於分類問題。每個類別都會與每個樣本的機率相關聯。如果我們確切知道應該是什麼,就可以將該機率設為 1,其他機率則設為 0。如要進一步瞭解 One-Hot 編碼,請參閱這個頁面

我們要監控的另一個指標是 accuracy,在分類問題中,這是指所有預測中正確預測所占的百分比。

6. 訓練模型

96914ff65fc3b74c.png將下列函式複製到 script.js 檔案。

async function train(model, data) {
  const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];
  const container = {
    name: 'Model Training', tab: 'Model', styles: { height: '1000px' }
  };
  const fitCallbacks = tfvis.show.fitCallbacks(container, metrics);
  
  const BATCH_SIZE = 512;
  const TRAIN_DATA_SIZE = 5500;
  const TEST_DATA_SIZE = 1000;

  const [trainXs, trainYs] = tf.tidy(() => {
    const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
    return [
      d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
      d.labels
    ];
  });

  const [testXs, testYs] = tf.tidy(() => {
    const d = data.nextTestBatch(TEST_DATA_SIZE);
    return [
      d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
      d.labels
    ];
  });

  return model.fit(trainXs, trainYs, {
    batchSize: BATCH_SIZE,
    validationData: [testXs, testYs],
    epochs: 10,
    shuffle: true,
    callbacks: fitCallbacks
  });
}

96914ff65fc3b74c.png 接著,將下列程式碼加到

run 函式。

const model = getModel();
tfvis.show.modelSummary({name: 'Model Architecture', tab: 'Model'}, model);
  
await train(model, data);

重新整理頁面,幾秒後您應該會看到一些圖表,顯示訓練進度。

a2c7628dc47d465.png

讓我們來深入瞭解。

監控指標

const metrics = ['loss', 'val_loss', 'acc', 'val_acc'];

我們將在此決定要監控哪些指標。我們會監控訓練集的損失和準確率,以及驗證集的損失和準確率 (分別為 val_loss 和 val_acc)。我們會在下文進一步說明驗證集。

以張量形式準備資料

const BATCH_SIZE = 512;
const TRAIN_DATA_SIZE = 5500;
const TEST_DATA_SIZE = 1000;

const [trainXs, trainYs] = tf.tidy(() => {
  const d = data.nextTrainBatch(TRAIN_DATA_SIZE);
  return [
    d.xs.reshape([TRAIN_DATA_SIZE, 28, 28, 1]),
    d.labels
  ];
});

const [testXs, testYs] = tf.tidy(() => {
  const d = data.nextTestBatch(TEST_DATA_SIZE);
  return [
    d.xs.reshape([TEST_DATA_SIZE, 28, 28, 1]),
    d.labels
  ];
});

這裡我們建立兩個資料集:一個是訓練集,用來訓練模型;另一個是驗證集,用來在每個訓練週期結束時測試模型,但驗證集中的資料在訓練期間不會顯示給模型。

我們提供的資料類別可輕鬆從圖片資料取得張量。不過,我們仍會將張量重塑為模型預期的形狀 [num_examples, image_width, image_height, channels],才能將這些張量提供給模型。每個資料集都有輸入內容 (X) 和標籤 (Y)。

return model.fit(trainXs, trainYs, {
  batchSize: BATCH_SIZE,
  validationData: [testXs, testYs],
  epochs: 10,
  shuffle: true,
  callbacks: fitCallbacks
});

我們呼叫 model.fit 來啟動訓練迴圈。我們也會傳遞 validationData 屬性,指出模型應在每個週期後用來測試自己的資料 (但不會用於訓練)。

如果模型在訓練資料上表現良好,但在驗證資料上表現不佳,表示模型可能過度配適訓練資料,無法妥善一般化先前未見的輸入內容。

7. 評估模型

驗證準確率可有效估算模型處理先前未見資料的成效 (只要該資料與驗證集在某方面相似)。不過,我們可能需要更詳細的各類別成效細目。

tfjs-vis 中有幾種方法可協助您完成這項操作。

96914ff65fc3b74c.png 將下列程式碼新增至 script.js 檔案底部

const classNames = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];

function doPrediction(model, data, testDataSize = 500) {
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const testData = data.nextTestBatch(testDataSize);
  const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
  const labels = testData.labels.argMax(-1);
  const preds = model.predict(testxs).argMax(-1);

  testxs.dispose();
  return [preds, labels];
}


async function showAccuracy(model, data) {
  const [preds, labels] = doPrediction(model, data);
  const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
  const container = {name: 'Accuracy', tab: 'Evaluation'};
  tfvis.show.perClassAccuracy(container, classAccuracy, classNames);

  labels.dispose();
}

async function showConfusion(model, data) {
  const [preds, labels] = doPrediction(model, data);
  const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
  const container = {name: 'Confusion Matrix', tab: 'Evaluation'};
  tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});

  labels.dispose();
}

這段程式碼的作用是什麼?

  • 進行預測。
  • 計算準確度指標。
  • 顯示指標

接著我們將進一步說明各項步驟。

預測

function doPrediction(model, data, testDataSize = 500) {
  const IMAGE_WIDTH = 28;
  const IMAGE_HEIGHT = 28;
  const testData = data.nextTestBatch(testDataSize);
  const testxs = testData.xs.reshape([testDataSize, IMAGE_WIDTH, IMAGE_HEIGHT, 1]);
  const labels = testData.labels.argMax(-1);
  const preds = model.predict(testxs).argMax(-1);

  testxs.dispose();
  return [preds, labels];
}      

首先,我們需要進行一些預測。我們將在此擷取 500 張圖片,並預測圖片中的數字 (您稍後可以增加這個數字,以便測試更多圖片)。

特別是 argmax 函式,可提供機率最高的類別索引。請注意,模型會輸出每個類別的機率。我們會找出機率最高的結果,並將其指派為預測結果。

您可能也會發現,我們可以一次對所有 500 個範例進行預測。這就是 TensorFlow.js 提供的向量化功能。

顯示各類別的準確率

async function showAccuracy() {
  const [preds, labels] = doPrediction();
  const classAccuracy = await tfvis.metrics.perClassAccuracy(labels, preds);
  const container = { name: 'Accuracy', tab: 'Evaluation' };
  tfvis.show.perClassAccuracy(container, classAccuracy, classNames);

  labels.dispose();
}      

有了預測和標籤,我們就能計算每個類別的準確率。

顯示混淆矩陣

async function showConfusion() {
  const [preds, labels] = doPrediction();
  const confusionMatrix = await tfvis.metrics.confusionMatrix(labels, preds);
  const container = { name: 'Confusion Matrix', tab: 'Evaluation' };
  tfvis.render.confusionMatrix(container, {values: confusionMatrix, tickLabels: classNames});

  labels.dispose();
}  

混淆矩陣與每個類別的準確率類似,但會進一步細分,顯示分類錯誤的模式。這有助於判斷模型是否會混淆任何特定類別組合。

顯示評估結果

96914ff65fc3b74c.png 在 run 函式的底部新增下列程式碼,顯示評估結果。

await showAccuracy(model, data);
await showConfusion(model, data);

畫面會顯示類似以下的內容。

82458197bd5e7f52.png

恭喜!您剛訓練完卷積類神經網路!

8. 主要重點

預測輸入資料的類別稱為分類工作。

分類工作需要適當的資料表示法來呈現標籤

  • 常見的標籤表示法包括類別的 one-hot 編碼

準備資料:

  • 建議保留部分資料,不要在訓練期間提供給模型,以便評估模型。這就是所謂的驗證集。

建構及執行模型:

  • 卷積模型在圖像工作方面表現優異。
  • 分類問題通常會使用類別交叉熵做為損失函式。
  • 監控訓練,查看損失是否下降,準確率是否上升。

評估模型

  • 訓練模型後,請決定評估方式,瞭解模型在您想解決的初始問題上表現如何。
  • 與整體準確率相比,每個類別的準確率和混淆矩陣可提供更精細的模型成效細目。