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

2020-10-19 17:53 更新

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


一、運(yùn)行代碼

這篇文章的全部代碼可以在倉(cāng)庫(kù) TensorFlow.js examples 中的 tfjs-examples/mnist 下找到,你可以通過下面的方式 clone 下來(lái)然后運(yùn)行這個(gè) demo

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

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

 

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

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

11

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

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

這個(gè) MnistData 有兩個(gè)共有方法:

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

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

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


三、創(chuàng)建模型

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

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

const model = tf.sequential();

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


四、添加第一層

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

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

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

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

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


五、添加第二層  

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

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

  注意:因?yàn)?code> poolSize 和 strides 都是2x2,所以池化層空口將會(huì)完全不會(huì)重疊。這也就意味著池化層將會(huì)把激活的大小從上一層減少一半。

 

六、添加剩下的層

重復(fù)使用層結(jié)構(gòu)是神經(jīng)網(wǎng)絡(luò)中的常見模式。我們添加第二個(gè)卷積層到模型,并在其后添加池化層。請(qǐng)注意,在我們的第二個(gè)卷積層中,我們將濾波器數(shù)量從8增加到16。還要注意,我們沒有指定 inputShape,因?yàn)樗梢詮那耙粚拥妮敵鲂螤钪型茢喑鰜?lái):

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

接下來(lái),我們添加一個(gè) flatten 層,將前一層的輸出平鋪到一個(gè)向量中:

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

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

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

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

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


定義優(yōu)化器

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

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


定義損失函數(shù)

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

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


定義評(píng)估指標(biāo)

對(duì)于我們的評(píng)估指標(biāo),我們將使用準(zhǔn)確度,該準(zhǔn)確度衡量所有預(yù)測(cè)中正確預(yù)測(cè)的百分比。


編譯模型

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

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;


進(jìn)一步了解分批量和批量大小

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

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

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

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

注意:*回想一下在我們的第一個(gè) 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并行化批量處理樣本,在對(duì)大量樣本進(jìn)行平均后才更新參數(shù):

const batch = data.nextTrainBatch(BATCH_SIZE);
  • 每5個(gè) step(TEST_ITERATION_FREQUENCY),我們構(gòu)造一次 validationData,這是一個(gè)包含一批來(lái)自MNIST測(cè)試集的圖像及其相應(yīng)標(biāo)簽這兩個(gè)元素的數(shù)組,我們將使用這些數(shù)據(jù)來(lái)評(píng)估模型的準(zhǔn)確性:
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í)際更新的地方。

注意:在整個(gè)數(shù)據(jù)集上執(zhí)行一次 model.fit 會(huì)導(dǎo)致將整個(gè)數(shù)據(jù)集上傳到 GPU,這可能會(huì)使應(yīng)用程序死機(jī)。 為避免向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});

我們?cè)賮?lái)分析一下這些參數(shù):

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

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

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


查看結(jié)果!

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

v2-22a6180e611dc02770481d6de33f9396_hd


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)