這篇文章中,我們將使用 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è)分辨器的精確度。
這篇文章的全部代碼可以在倉(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)目。
這里我們將會(huì)使用 MNIST
的手寫數(shù)據(jù),這些我們將要去分辨的手寫數(shù)據(jù)如下所示:
為了預(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è)。
在這一部分,我們將會(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ù)通常是 softmax
。 Softmax
將我們的10維輸出向量歸一化為概率分布,使得我們10個(gè)類中的每個(gè)都有一個(gè)概率值。對(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);
對(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
。
對(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;
為了充分利用 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)的代碼:
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);
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];
如果你運(yùn)行完整的代碼,你應(yīng)該看到這樣的輸出:
更多建議: