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

1. 簡介

在本教學課程中,我們會建構 TensorFlow.js 模型,透過卷積類神經網路辨識手寫數字。首先,我們要訓練分類器的「樣式」大量手寫數字與標籤接著,我們會使用模型從未見過的測試資料,評估分類器的準確度。

這項工作視為分類工作,因為我們正在訓練模型,將類別 (出現在圖片中的數字) 指派給輸入圖片。為了訓練模型,我們會提供許多輸入內容範例和正確輸出內容。這項功能稱為「監督式學習」

建構目標

您將建立一個使用 TensorFlow.js 在瀏覽器中訓練模型的網頁。如果圖片有特定大小的黑白圖片,系統會將其分類。您需要執行的步驟包括:

  • 載入資料。
  • 定義模型的架構。
  • 訓練模型並在訓練期間監控成效。
  • 進行預測以評估訓練過的模型。

課程內容

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

軟硬體需求

您也應該已經熟悉我們的第一個訓練教學課程中的教材。

2. 做好準備

建立 HTML 網頁並加入 JavaScript

96914ff65fc3b74c.png將下列程式碼複製到名為「 」的 HTML 檔案。

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. 在步驟 1 的資料夾中,建立一個名為 script.js 的檔案,然後將下列程式碼加入其中。
console.log('Hello TensorFlow');

測試

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

如果一切運作正常,應該建立兩個全域變數。tf 是 TensorFlow.js 程式庫的參照,tfvistfjs-vis 程式庫的參照。

系統應該會顯示 Hello TensorFlow, 訊息,表示您已經準備好進行下一個步驟。

3. 載入資料

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

天母 4 天母 3 天母 8

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

歡迎您自行研究 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) 的分數

每張圖片的高度都是 28px,高 28px,有 1 個色彩頻道,因為它是灰階圖片。因此每張圖片的形狀都是 [28, 28, 1]

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

5. 定義模型架構

在本節中,我們將撰寫程式碼來說明模型架構。模型架構可讓您清楚表示「模型在執行時會執行哪些函式」,或「Google 模型會使用何種演算法計算答案」

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

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。要套用至輸入資料的滑動卷積篩選視窗大小。這裡,我們設定 5kernelSize,指定 5x5 卷積窗口。
  • filters。要套用至輸入資料的篩選器視窗數量 (大小為 kernelSize)。這樣我們就會對資料套用 8 個篩選器。
  • strides。「步距」也就是滑動視窗隨著圖片移動的像素數量我們在此指定的平方值是 1,表示濾鏡會在圖片上滑動 1 像素的步數。
  • activation。卷積後要套用至資料的啟用函式。在這個例子中,我們會套用「Rectified Linear Unit (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

實際標籤

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 來開始訓練迴圈。我們也會傳遞 VerifyData 屬性,以指出模型在每個訓練週期後應使用哪些資料來自我測試 (但不得用於訓練)。

如果我們的訓練資料依舊有效,但驗證資料沒有問題,就表示模型可能過度配適於訓練資料,因此便無法妥善一般化,以便輸入先前沒看過的內容。

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 將下列程式碼新增至執行函式底部,以顯示評估作業。

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

畫面應如下所示。

82458197bd5e7f52.png

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

8. 重點整理

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

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

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

準備資料:

  • 除了模型在訓練期間看不到的資料外,您還可以保留一些資料來評估模型成效。這就是驗證集。

建構並執行模型:

  • 研究顯示,卷積模型可在圖片工作上發揮良好成效。
  • 分類問題通常會使用類別交叉熵來處理損失函式。
  • 監控訓練,看看損失是否減少,準確性也會上升。

評估模型

  • 決定模型訓練完畢後,要評估模型在最初問題解決方面的成效。
  • 比起只評估整體準確率,依類別準確率與混淆矩陣相較,您可以更精細地分析模型成效。