這篇文章中,我們將使用 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)建你個人的項目。
這里我們將會使用 MNIST
的手寫數(shù)據(jù),這些我們將要去分辨的手寫數(shù)據(jù)如下所示:
為了預(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)建一個卷積圖片識別模型。為了這樣做,我們使用了 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個像素的方式來劃過圖片。 注意:因為 poolSize
和 strides
都是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ù)通常是 softmax
。 Softmax
將我們的10維輸出向量歸一化為概率分布,使得我們10個類中的每個都有一個概率值。對于我們的卷積神經(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ù),我們將使用通常用于優(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)的代碼:
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);
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];
如果你運行完整的代碼,你應(yīng)該看到這樣的輸出:
更多建議: