Tensorflow.js 圖片訓(xùn)練

2020-10-19 17:53 更新

這篇文章中,我們將使用 CNN 構(gòu)建一個 Tensorflow.js 模型來分辨手寫的數(shù)字。首先,我們通過使之“查看”數(shù)以千計的數(shù)字圖片以及他們對應(yīng)的標識來訓(xùn)練分辨器。然后我們再通過此模型從未“見到”過的測試數(shù)據(jù)評估這個分辨器的精確度。


一、運行代碼

這篇文章的全部代碼可以在倉庫 TensorFlow.js examples 中的 tfjs-examples/mnist 下找到,你可以通過下面的方式 clone 下來然后運行這個 demo

$ git clone https://github.com/tensorflow/tfjs-examples
$ cd tfjs-examples/mnist
$ yarn
$ yarn watch

上面的這個目錄完全是獨立的,所以完全可以 copy 下來然后創(chuàng)建你個人的項目。

 

二、數(shù)據(jù)相關(guān)

這里我們將會使用  MNIST  的手寫數(shù)據(jù),這些我們將要去分辨的手寫數(shù)據(jù)如下所示:

11

為了預(yù)處理這些數(shù)據(jù),我們已經(jīng)寫了  data.js, 這個文件包含了 Minsdata 類,而這個類可以幫助我們從 MNIST 的數(shù)據(jù)集中獲取到任意的一些列的 MNIST。

MnistData這個類將全部的數(shù)據(jù)分割成了訓(xùn)練數(shù)據(jù)和測試數(shù)據(jù)。我們訓(xùn)練模型的時候,分辨器就會只觀察訓(xùn)練數(shù)據(jù)。而當(dāng)我們評價模型時,我們就僅僅使用測試數(shù)據(jù),而這些測試數(shù)據(jù)是模型還沒有看見到的,這樣就可以來觀察模型預(yù)測全新的數(shù)據(jù)了。

這個 MnistData 有兩個共有方法:

1、nextTrainBatch(batchSize): 從訓(xùn)練數(shù)據(jù)中返回一批任意的圖片以及他們的標識。

2、nextTestBatch(batchSize):  從測試數(shù)據(jù)中返回一批圖片以及他們的標識。

注意:當(dāng)我們訓(xùn)練 MNIST 分辨器時,應(yīng)當(dāng)注意數(shù)據(jù)獲取的任意性是非常重要的,這樣模型預(yù)測才不會受到我們提供圖片順序的干擾。例如,如果我們每次給這個模型第一次都提供的是數(shù)字1,那么在訓(xùn)練期間,這個模型就會簡單的預(yù)測第一個就是 1(因為這樣可以減小損失函數(shù))。 而如果我們每次訓(xùn)練時都提供的是 2,那么它也會簡單切換為預(yù)測 2 并且永遠不會預(yù)測 1(同樣的,也是因為這樣可以減少損失函數(shù))。如果每次都提供這樣典型的、有代表性的數(shù)字,那么這個模型將永遠也學(xué)不會做出一個精確的預(yù)測。


三、創(chuàng)建模型

在這一部分,我們將會創(chuàng)建一個卷積圖片識別模型。為了這樣做,我們使用了 Sequential 模型(模型中最為簡單的一個類型),在這個模型中,張量(tensors)可以連續(xù)的從一層傳遞到下一層中。

  首先,我們需要使用 tf.sequential 先初始化一個 sequential 模型:

const model = tf.sequential();

既然我們已經(jīng)創(chuàng)建了一個模型,那么我們就可以添加層了。


四、添加第一層

我們要添加的第一層是一個 2 維的卷積層。卷積將過濾窗口掠過圖片來學(xué)習(xí)空間上來說不會轉(zhuǎn)變的變量(即圖片中不同位置的模式或者物體將會被平等對待)。

我們可以通過 tf.layers.conv2d 來創(chuàng)建一個2維的卷積層,這個卷積層可以接受一個配置對象來定義層的結(jié)構(gòu),如下所示:

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

讓我們拆分對象中的每個參數(shù)吧:

  • inputShape。這個數(shù)據(jù)的形狀將回流入模型的第一層。在這個示例中,我們的 MNIST 例子是 28 x 28 像素的黑白圖片,這個關(guān)于圖片的特定的格式即 [row, column, depth],所以我們想要配置一個[28, 28, 1] 的形狀,其中28行和28列是這個數(shù)字在每個維度上的像素數(shù),且其深度為 1,這是因為我們的圖片只有1個顏色:
  • kernelSize。劃過卷積層過濾窗口的數(shù)量將會被應(yīng)用到輸入數(shù)據(jù)中去。這里,我們設(shè)置了 kernalSize 的值為5,也就是指定了一個5 x 5的卷積窗口。
  • filters。這個 kernelSize 的過濾窗口的數(shù)量將會被應(yīng)用到輸入數(shù)據(jù)中,我們這里將8個過濾器應(yīng)用到數(shù)據(jù)中。
  • strides。 即滑動窗口每一步的步長。比如每當(dāng)過濾器移動過圖片時將會由多少像素的變化。這里,我們指定其步長為1,這意味著每一步都是1像素的移動。
  • activation。這個 activation 函數(shù)將會在卷積完成之后被應(yīng)用到數(shù)據(jù)上。在這個例子中,我們應(yīng)用了 relu 函數(shù),這個函數(shù)在機器學(xué)習(xí)中是一個非常常見的激活函數(shù)。
  • kernelInitializer。這個方法對于訓(xùn)練動態(tài)的模型是非常重要的,他被用于任意地初始化模型的 weights。我們這里將不會深入細節(jié)來講,但是 VarianceScaling (即這里用的)真的是一個初始化非常好的選擇。


五、添加第二層  

讓我們?yōu)檫@個模型添加第二層:一個最大的池化層(pooling layer),這個層中我們將通過 tf.layers.maxPooling2d 來創(chuàng)建。這一層將會通過在每個滑動窗口中計算最大值來降頻取樣得到結(jié)果。

model.add(tf.layers.maxPooling2d({
  poolSize: [2, 2],
  strides: [2, 2]
}));
  • poolSize。這個滑動池窗口的數(shù)量將會被應(yīng)用到輸入的數(shù)據(jù)中。這里我們設(shè)置 poolSize為[2, 2],所以這就意味著池化層將會對輸入數(shù)據(jù)應(yīng)用2x2的窗口。
  • strides。 這個池化層的步長大小。比如,當(dāng)每次挪開輸入數(shù)據(jù)時窗口需要移動多少像素。這里我們指定 strides為[2, 2],這就意味著過濾器將會以在水平方向和豎直方向上同時移動2個像素的方式來劃過圖片。

  注意:因為 poolSizestrides 都是2x2,所以池化層空口將會完全不會重疊。這也就意味著池化層將會把激活的大小從上一層減少一半。

 

六、添加剩下的層

重復(fù)使用層結(jié)構(gòu)是神經(jīng)網(wǎng)絡(luò)中的常見模式。我們添加第二個卷積層到模型,并在其后添加池化層。請注意,在我們的第二個卷積層中,我們將濾波器數(shù)量從8增加到16。還要注意,我們沒有指定 inputShape,因為它可以從前一層的輸出形狀中推斷出來:

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]
}));

接下來,我們添加一個 flatten 層,將前一層的輸出平鋪到一個向量中:

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

最后,讓我們添加一個 dense 層(也稱為全連接層),它將執(zhí)行最終的分類。 在 dense 層前先對卷積+池化層的輸出執(zhí)行 flatten 也是神經(jīng)網(wǎng)絡(luò)中的另一種常見模式:

model.add(tf.layers.dense({
  units: 10,
  kernelInitializer: 'VarianceScaling',
  activation: 'softmax'
}));

我們來分析傳遞給 dense 層的參數(shù)。

    /
  • units. 激活輸出的數(shù)量。由于這是最后一層,我們正在做10個類別的分類任務(wù)(數(shù)字0-9),因此我們在這里使用10個 units。 (有時 units 被稱為神經(jīng)元的數(shù)量,但我們會避免使用該術(shù)語。)
  • kernelInitializer. 我們將對 dense 層使用與卷積層相同的 VarianceScaling 初始化策略。
  • activation. 分類任務(wù)的最后一層的激活函數(shù)通常是 softmaxSoftmax 將我們的10維輸出向量歸一化為概率分布,使得我們10個類中的每個都有一個概率值。


定義優(yōu)化器

對于我們的卷積神經(jīng)網(wǎng)絡(luò)模型,我們將使用學(xué)習(xí)率為0.15的隨機梯度下降(SGD)優(yōu)化器:

const LEARNING_RATE = 0.15;
const optimizer = tf.train.sgd(LEARNING_RATE);


定義損失函數(shù)

對于損失函數(shù),我們將使用通常用于優(yōu)化分類任務(wù)的交叉熵( categoricalCrossentropy)。 categoricalCrossentropy 度量模型的最后一層產(chǎn)生的概率分布與標簽給出的概率分布之間的誤差,這個分布在正確的類標簽中為1(100%)。 例如,下面是數(shù)字7的標簽和預(yù)測值:

|class | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |----------|---|---|---|---|---|---|---|---|---|---| |label | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | |prediction|.1 |.01|.01|.01|.20|.01|.01|.60|.03|.02|

如果預(yù)測的結(jié)果是數(shù)字7的概率很高,那么 categoricalCrossentropy 會給出一個較低的損失值,而如果7的概率很低,那么 categoricalCrossentropy 的損失就會更高。在訓(xùn)練過程中,模型會更新它的內(nèi)部參數(shù)以最小化在整個數(shù)據(jù)集上的 categoricalCrossentropy


定義評估指標

對于我們的評估指標,我們將使用準確度,該準確度衡量所有預(yù)測中正確預(yù)測的百分比。


編譯模型

為了編譯模型,我們傳入一個由優(yōu)化器,損失函數(shù)和一系列評估指標(這里只是'精度')組成的配置對象:

model.compile({
  optimizer: optimizer,
  loss: 'categoricalCrossentropy',
  metrics: ['accuracy'],
});


配置批量大小

在開始訓(xùn)練之前,我們需要定義一些與 batch size 相關(guān)的參數(shù):

const BATCH_SIZE = 64;
const TRAIN_BATCHES = 100;
const TEST_BATCH_SIZE = 1000;
const TEST_ITERATION_FREQUENCY = 5;


進一步了解分批量和批量大小

為了充分利用 GPU 并行化計算的能力,我們希望將多個輸入批量處理,并使用單個前饋網(wǎng)絡(luò)調(diào)用將他們饋送到網(wǎng)絡(luò)。

我們批量計算的另一個原因是,在優(yōu)化過程中,我們只能在對多個樣本中的梯度進行平均后更新內(nèi)部參數(shù)(邁出一步)。這有助于我們避免因錯誤的樣本(例如錯誤標記的數(shù)字)而朝錯誤的方向邁出了一步。

當(dāng)批量輸入數(shù)據(jù)時,我們引入秩 D + 1 的張量,其中D是單個輸入的維數(shù)。

如前所述,我們 MNIST 數(shù)據(jù)集中單個圖像的維度為[28,28,1]。當(dāng)我們將 BATCH_SIZE 設(shè)置為64時,我們每次批量處理64個圖像,這意味著我們的數(shù)據(jù)的實際形狀是[64,28,28,1](批量始終是最外層的維度)。

注意:*回想一下在我們的第一個 conv2d 配置中的 inputShape 沒有指定批量大?。?4)。 Config 被寫成批量大小不可知的,以便他們能夠接受任意大小的批次。


編寫訓(xùn)練循環(huán)

以下是訓(xùn)練循環(huán)的代碼:

for (let i = 0; i < TRAIN_BATCHES; i++) {
  const batch = data.nextTrainBatch(BATCH_SIZE);
 
  let testBatch;
  let validationData;

  if (i % TEST_ITERATION_FREQUENCY === 0) {
    testBatch = data.nextTestBatch(TEST_BATCH_SIZE);
    validationData = [
      testBatch.xs.reshape([TEST_BATCH_SIZE, 28, 28, 1]), testBatch.labels
    ];
  }
 
  const history = await model.fit(
      batch.xs.reshape([BATCH_SIZE, 28, 28, 1]),
      batch.labels,
      {
        batchSize: BATCH_SIZE,
        validationData,
        epochs: 1
      });

  const loss = history.history.loss[0];
  const accuracy = history.history.acc[0];

}

讓我們分析代碼。 首先,我們獲取一批訓(xùn)練樣本。 回想一下上面說的,我們利用GPU并行化批量處理樣本,在對大量樣本進行平均后才更新參數(shù):

const batch = data.nextTrainBatch(BATCH_SIZE);
  • 每5個 step(TEST_ITERATION_FREQUENCY),我們構(gòu)造一次 validationData,這是一個包含一批來自MNIST測試集的圖像及其相應(yīng)標簽這兩個元素的數(shù)組,我們將使用這些數(shù)據(jù)來評估模型的準確性:
if (i % TEST_ITERATION_FREQUENCY === 0) {
  testBatch = data.nextTestBatch(TEST_BATCH_SIZE);
  validationData = [
    testBatch.xs.reshape([TEST_BATCH_SIZE, 28, 28, 1]),
    testBatch.labels
  ];
}

model.fit 是模型訓(xùn)練和參數(shù)實際更新的地方。

注意:在整個數(shù)據(jù)集上執(zhí)行一次 model.fit 會導(dǎo)致將整個數(shù)據(jù)集上傳到 GPU,這可能會使應(yīng)用程序死機。 為避免向GPU上傳太多數(shù)據(jù),我們建議在 for 循環(huán)中調(diào)用 model.fit(),一次傳遞一批數(shù)據(jù),如下所示:

  const history = await model.fit(
      batch.xs.reshape([BATCH_SIZE, 28, 28, 1]), batch.labels,
      {batchSize: BATCH_SIZE, validationData: validationData, epochs: 1});

我們再來分析一下這些參數(shù):

  • X. 輸入圖像數(shù)據(jù)。請記住,我們分批量提供樣本,因此我們必須告訴fit函數(shù)batch有多大。 MnistData.nextTrainBatch 返回形狀為[BATCH_SIZE,784]的圖像 —— 所有的圖像數(shù)據(jù)是長度為784(28 * 28)的一維向量。但是,我們的模型預(yù)期圖像數(shù)據(jù)的形狀為[BATCH_SIZE,28,28,1],因此我們需要使用reshape函數(shù)。
  • y. 我們的標簽;每個圖像的正確數(shù)字分類。
  • BATCHSIZE. 每個訓(xùn)練 batch 中包含多少個圖像。之前我們在這里設(shè)置的BATCH_SIZE是 64。
  • validationData. 每隔TEST_ITERATION_FREQUENCY(這里是5)個Batch,我們構(gòu)建的驗證集。該數(shù)據(jù)的形狀為[TEST_BATCH_SIZE,28,28,1]。之前,我們設(shè)置了1000的TEST_BATCH_SIZE。我們的評估度量(準確度)將在此數(shù)據(jù)集上計算。
  • epochs. 批量執(zhí)行的訓(xùn)練次數(shù)。由于我們分批把數(shù)據(jù)饋送到fit函數(shù),所以我們希望它每次僅從這個 batch 上進行訓(xùn)練。

每次調(diào)用 fit 的時候,它會返回一個包含指標日志的對象,我們把它存儲在 history。我們提取每次訓(xùn)練迭代的損失和準確度,以便將它們繪制在圖上:

const loss = history.history.loss[0];
const accuracy = history.history.acc[0];


查看結(jié)果!

如果你運行完整的代碼,你應(yīng)該看到這樣的輸出:

v2-22a6180e611dc02770481d6de33f9396_hd


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號