?第2章 神經(jīng)網(wǎng)絡(luò)的數(shù)學(xué)基礎(chǔ)
2.1 初識(shí)神經(jīng)網(wǎng)絡(luò)
我們來(lái)看一個(gè)具體的神經(jīng)網(wǎng)絡(luò)示例,使用 Python 的 Keras 庫(kù) 來(lái)學(xué)習(xí)手寫數(shù)字分類。
我們這里要解決的問(wèn)題是, 將手寫數(shù)字的灰度圖像(28 像素×28 像素)劃分到 10 個(gè)類別 中(0~9) 。我們將使用 MNIST 數(shù)據(jù)集 ,它是機(jī)器學(xué)習(xí)領(lǐng)域的一個(gè)經(jīng)典數(shù)據(jù)集,其歷史幾乎和這 個(gè)領(lǐng)域一樣長(zhǎng),而且已被人們深入研究。
MNIST 數(shù)據(jù)集包含 60 000 張訓(xùn)練圖像 和 10 000 張測(cè)試圖 像 ,由美國(guó)國(guó)家標(biāo)準(zhǔn)與技術(shù)研究院( National Institute of Standards and Technology ,即 MNIST 中 的 NIST )在 20 世紀(jì) 80 年代收集得到。你可以將“解決” MNIST 問(wèn)題看作深度學(xué)習(xí)的 “Hello World ”,正是用它來(lái)驗(yàn)證你的算法是否按預(yù)期運(yùn)行。
當(dāng)你成為機(jī)器學(xué)習(xí)從業(yè)者后,會(huì)發(fā)現(xiàn) MNIST 一次又一次地出現(xiàn)在科學(xué)論文、博客文章等中。圖 2-1 給出了 MNIST 數(shù)據(jù)集的一些樣本。
關(guān)于類和標(biāo)簽的說(shuō)明
在機(jī)器學(xué)習(xí)中, 分類問(wèn)題中的某個(gè)類別叫作類(class) 。 數(shù)據(jù)點(diǎn)叫作樣本(sample) 。 某個(gè)樣本對(duì)應(yīng)的類叫作標(biāo)簽(label) 。
MNIST 數(shù)據(jù)集預(yù)先加載在 Keras 庫(kù)中,其中包括 4 個(gè) Numpy 數(shù)組。
代碼清單 2-1 加載 Keras 中的 MNIST 數(shù)據(jù)集
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images 和 train_labels 組成了訓(xùn)練集(training set) ,模型將從這些數(shù)據(jù)中進(jìn)行學(xué)習(xí)。然后在 測(cè)試集(test set,即 test_images 和 test_labels)上對(duì)模型進(jìn)行測(cè)試 。
圖像被編碼為 Numpy 數(shù)組 ,而標(biāo)簽是數(shù)字?jǐn)?shù)組,取值范圍為 0~9 。圖像和標(biāo)簽一一對(duì)應(yīng)。
# 代碼清單 2-1 加載keras中的minst數(shù)據(jù)集
from keras.datasets import mnist
from keras.utils import to_categorical(train_images, train_labels), (test_images, test_labels) = mnist.load_data()print('train_images.shape:', train_images.shape)
print('len(train_labels)', len(train_labels))print('train_labels', train_labels)print('test_images.shape:', test_images.shape)
print(' len(test_labels):', len(test_labels))
print('test_labels:', test_labels)
運(yùn)行結(jié)果:
train_images.shape: (60000, 28, 28)
len(train_labels) 60000
train_labels [5 0 4 ... 5 6 8]
test_images.shape: (10000, 28, 28)len(test_labels): 10000
test_labels: [7 2 1 ... 4 5 6]
接下來(lái)的工作流程如下:
首先,將訓(xùn)練數(shù)據(jù)(train_images 和 train_labels)輸入神經(jīng)網(wǎng)絡(luò);
其次,網(wǎng)絡(luò)學(xué)習(xí)將圖像和標(biāo)簽關(guān)聯(lián)在一起;
最后,網(wǎng)絡(luò)對(duì) test_images 生成預(yù)測(cè), 而我們將驗(yàn)證這些預(yù)測(cè)與 test_labels 中的標(biāo)簽是否匹配。
# 代碼清單 2-2 網(wǎng)絡(luò)架構(gòu)
from keras import models
from keras import layersnetwork = models.Sequential()
# Dense 也就是全連接的神經(jīng)網(wǎng)絡(luò)
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
神經(jīng)網(wǎng)絡(luò)的核心組件是層(layer) , 它是一種數(shù)據(jù)處理模塊,你可以將它看成數(shù)據(jù)過(guò)濾器。
進(jìn)去一些數(shù)據(jù),出來(lái)的數(shù)據(jù)變得更加有用。具體來(lái)說(shuō),層從輸入數(shù)據(jù)中提取表示——我們期望 這種表示有助于解決手頭的問(wèn)題。大多數(shù)深度學(xué)習(xí)都是將簡(jiǎn)單的層鏈接起來(lái),從而實(shí)現(xiàn)漸進(jìn)式的 數(shù)據(jù)蒸餾(data distillation )。深度學(xué)習(xí)模型就像是數(shù)據(jù)處理的篩子,包含一系列越來(lái)越精細(xì)的
數(shù)據(jù)過(guò)濾器(即層)。
本例中的網(wǎng)絡(luò)包含 2 個(gè) Dense 層 ,它們是 密集連接(也叫全連接)的神經(jīng)層 。
第二層(也是最后一層)是一個(gè) 10 路 softmax 層 ,它將返回一個(gè)由 10 個(gè)概率值(總和為 1) 組成的數(shù)組。
每個(gè)概率值表示當(dāng)前數(shù)字圖像屬于 10 個(gè)數(shù)字類別中某一個(gè)的概率。
要想訓(xùn)練網(wǎng)絡(luò),我們還需要選擇 編譯(compile)步驟的三個(gè)參數(shù) 。
損失函數(shù)(loss function) :網(wǎng)絡(luò)如何衡量在訓(xùn)練數(shù)據(jù)上的性能,即網(wǎng)絡(luò)如何朝著正確的方向前進(jìn)。
優(yōu)化器(optimizer) :基于訓(xùn)練數(shù)據(jù)和損失函數(shù)來(lái)更新網(wǎng)絡(luò)的機(jī)制。
在訓(xùn)練和測(cè)試過(guò)程中需要監(jiān)控的指標(biāo)(metric) :本例只關(guān)心 精度 ,即正確分類的圖像所占的比例。
后續(xù)兩章會(huì)詳細(xì)解釋損失函數(shù)和優(yōu)化器的確切用途。
# 2.3 編譯步驟
# 編譯的三個(gè)參數(shù):損失函數(shù)、優(yōu)化器、監(jiān)控的指標(biāo)(精度)
network.compile(optimizer='rmsprop',loss='categorical_crossentropy',metrics=['accuracy'])
在開(kāi)始訓(xùn)練之前,我們將對(duì)數(shù)據(jù)進(jìn)行預(yù)處理 ,將其變換為網(wǎng)絡(luò)要求的形狀,并縮放到所有值都在 [0, 1] 區(qū)間。比如,之前訓(xùn)練圖像保存在一個(gè) uint8 類型 的數(shù)組中,其形狀為 (60000, 28, 28) ,取值區(qū)間為 [0, 255] 。我們需要將其變換為一個(gè) float32 數(shù)組,其形狀為 (60000, 28 * 28) ,取值范圍為 0~1 。
# 2-4 準(zhǔn)備圖像數(shù)據(jù)
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
我們還需要對(duì)標(biāo)簽進(jìn)行分類編碼,第 3 章將會(huì)對(duì)這一步驟進(jìn)行解釋。
# 2-5準(zhǔn)備標(biāo)簽
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)network.fit(train_images, train_labels, epochs=5, batch_size=128)test_loss, test_acc = network.evaluate(test_images, test_labels)
print('test_acc:', test_acc)
現(xiàn)在我們準(zhǔn)備開(kāi)始訓(xùn)練網(wǎng)絡(luò),在 Keras 中這一步是通過(guò)調(diào)用網(wǎng)絡(luò)的 fit 方法來(lái)完成的 —— 我們?cè)谟?xùn)練數(shù)據(jù)上 擬合 ( fit )模型。
469/469 [==============================] - 1s 2ms/step - loss: 0.2649 - accuracy: 0.9234
Epoch 2/5
469/469 [==============================] - 1s 2ms/step - loss: 0.1077 - accuracy: 0.9678
Epoch 3/5
469/469 [==============================] - 1s 2ms/step - loss: 0.0712 - accuracy: 0.9790
Epoch 4/5
469/469 [==============================] - 1s 2ms/step - loss: 0.0508 - accuracy: 0.9847
Epoch 5/5
469/469 [==============================] - 1s 2ms/step - loss: 0.0380 - accuracy: 0.9885
313/313 [==============================] - 0s 923us/step - loss: 0.0592 - accuracy: 0.9812
test_acc: 0.9811999797821045
訓(xùn)練過(guò)程中顯示了兩個(gè)數(shù)字:一個(gè)是網(wǎng)絡(luò)在訓(xùn)練數(shù)據(jù)上的 損失(loss) ,另一個(gè)是網(wǎng)絡(luò)在訓(xùn)練數(shù)據(jù)上的 精度(acc )。
我們很快就在訓(xùn)練數(shù)據(jù)上達(dá)到了 0.989 ( 98.9%)的精度?,F(xiàn)在我們來(lái)檢查一下模型在測(cè)試集上的性能。
測(cè)試集精度為 97.8% ,比訓(xùn)練集精度低不少。 訓(xùn)練精度和測(cè)試精度之間的這種差距是 過(guò)擬合(overfit) 造成的。過(guò)擬合是指機(jī)器學(xué)習(xí)模型在新數(shù)據(jù)上的性能往往比在訓(xùn)練數(shù)據(jù)上要差,它是第 3 章的核心主題。
第一個(gè)例子到這里就結(jié)束了。你剛剛看到了如何構(gòu)建和訓(xùn)練一個(gè)神經(jīng)網(wǎng)絡(luò),用不到 20 行的
Python 代碼對(duì)手寫數(shù)字進(jìn)行分類。
下一章會(huì)詳細(xì)介紹這個(gè)例子中的每一個(gè)步驟,并講解其背后的原理。接下來(lái)你將要學(xué)到張量(輸入網(wǎng)絡(luò)的數(shù)據(jù)存儲(chǔ)對(duì)象)、張量運(yùn)算(層的組成要素)和梯度下降(可以讓網(wǎng)絡(luò)從訓(xùn)練樣本中進(jìn)行學(xué)習(xí))。
2.2 神經(jīng)網(wǎng)絡(luò)的數(shù)據(jù)表示
前面例子使用的數(shù)據(jù)存儲(chǔ)在多維 Numpy 數(shù)組中,也叫張量(tensor)。一般來(lái)說(shuō),當(dāng)前所有機(jī)器學(xué)習(xí)系統(tǒng)都使用張量作為基本數(shù)據(jù)結(jié)構(gòu)。張量對(duì)這個(gè)領(lǐng)域非常重要,重要到 Google 的TensorFlow 都以它來(lái)命名。
張量這一概念的核心在于,它是一個(gè)數(shù)據(jù)容器。它包含的數(shù)據(jù)幾乎總是數(shù)值數(shù)據(jù),因此它是數(shù)字的容器。你可能對(duì)矩陣很熟悉,它是二維張量。張量是矩陣向任意維度的推廣[注意,張量的維度(dimension)通常叫作軸(axis)]。
2.2.1 標(biāo)量(0D 張量)
僅包含一個(gè)數(shù)字的張量叫作標(biāo)量(scalar,也叫標(biāo)量張量、零維張量、0D 張量)。在 Numpy中,一個(gè) float32 或 float64 的數(shù)字就是一個(gè)標(biāo)量張量(或標(biāo)量數(shù)組)。你可以用 ndim 屬性來(lái)查看一個(gè) Numpy 張量的軸的個(gè)數(shù)。標(biāo)量張量有 0 個(gè)軸(ndim == 0)。張量軸的個(gè)數(shù)也叫作階(rank)。下面是一個(gè) Numpy 標(biāo)量。
>>> import numpy as np
>>> x = np.array(12)
>>> x
array(12)
>>> x.ndim
0
2.2.2 向量(1D 張量)
數(shù)字組成的數(shù)組叫作向量(vector)或一維張量(1D 張量)。一維張量只有一個(gè)軸。下面是
一個(gè) Numpy 向量。
>>> x = np.array([12, 3, 6, 14, 7])
>>> x
array([12, 3, 6, 14, 7])
>>> x.ndim
1
這個(gè)向量有 5 個(gè)元素,所以被稱為 5D 向量。
不要把 5D 向量和 5D 張量弄混!
5D 向量只有一個(gè)軸,沿著軸有 5 個(gè)維度,而 5D 張量有 5 個(gè)軸(沿著每個(gè)軸可能有任意個(gè)維度)。
維度(dimensionality)可以表示沿著某個(gè)軸上的元素個(gè)數(shù)(比如 5D 向量),也可以表示張量中軸的個(gè)數(shù)(比如 5D 張量),這有時(shí)會(huì)令人感到混亂。
對(duì)于后一種情況,技術(shù)上更準(zhǔn)確的說(shuō)法是 5 階張量(張量的階數(shù)即軸的個(gè)數(shù)),但 5D 張量這種模糊的寫法更常見(jiàn)。
2.2.3 矩陣(2D 張量)
向量組成的數(shù)組叫作矩陣(matrix)或二維張量(2D 張量)。
矩陣有 2 個(gè)軸(通常叫作行和列)。你可以將矩陣直觀地理解為數(shù)字組成的矩形網(wǎng)格。
>>> x = np.array([[5, 78, 2, 34, 0],[6, 79, 3, 35, 1],[7, 80, 4, 36, 2]])
>>> x.ndim
2
第一個(gè)軸上的元素叫作行(row),第二個(gè)軸上的元素叫作列(column)。
在上面的例子中,[5, 78, 2, 34, 0] 是 x 的第一行,[5, 6, 7] 是第一列。
2.2.4? 3D 張量與更高維張量
將多個(gè)矩陣組合成一個(gè)新的數(shù)組,可以得到一個(gè) 3D 張量,你可以將其直觀地理解為數(shù)字組成的立方體。下面是一個(gè) Numpy 的 3D 張量。
>>> x = np.array([[[5, 78, 2, 34, 0],[6, 79, 3, 35, 1],[7, 80, 4, 36, 2]],[[5, 78, 2, 34, 0],[6, 79, 3, 35, 1],[7, 80, 4, 36, 2]],[[5, 78, 2, 34, 0],[6, 79, 3, 35, 1],[7, 80, 4, 36, 2]]])
>>> x.ndim
3
將多個(gè) 3D 張量組合成一個(gè)數(shù)組,可以創(chuàng)建一個(gè) 4D 張量,以此類推。深度學(xué)習(xí)處理的一般是 0D 到 4D 的張量,但處理視頻數(shù)據(jù)時(shí)可能會(huì)遇到 5D 張量。
2.2.5 關(guān)鍵屬性
張量是由以下三個(gè)關(guān)鍵屬性來(lái)定義的。
軸的個(gè)數(shù)(階)。例如,3D 張量有 3 個(gè)軸,矩陣有 2 個(gè)軸。這在 Numpy 等 Python 庫(kù)中也叫張量的 ndim。
形狀。這是一個(gè)整數(shù)元組,表示張量沿每個(gè)軸的維度大小(元素個(gè)數(shù))。例如,前面矩陣示例的形狀為 (3, 5),3D 張量示例的形狀為 (3, 3, 5)。向量的形狀只包含一個(gè)元素,比如 (5,),而標(biāo)量的形狀為空,即 ()。
數(shù)據(jù)類型(在 Python 庫(kù)中通常叫作 dtype)。這是張量中所包含數(shù)據(jù)的類型,例如,張量的類型可以是 float32、uint8、float64 等。
在極少數(shù)情況下,你可能會(huì)遇到字符(char)張量。注意,Numpy(以及大多數(shù)其他庫(kù))中不存在字符串張量,因?yàn)閺埩看鎯?chǔ)在預(yù)先分配的連續(xù)內(nèi)存段中,而字符串的長(zhǎng)度是可變的,無(wú)法用這種方式存儲(chǔ)。
為了具體說(shuō)明,我們回頭看一下 MNIST 例子中處理的數(shù)據(jù)。首先加載 MNIST 數(shù)據(jù)集。
from keras.datasets import mnist(train_images, train_labels), (test_images, test_labels) = mnist.load_data()print('train_images.ndim:',train_images.ndim)
print('train_images.shape:',train_images.shape)
print('train_images.dtype:',train_images.dtype)
train_images.ndim: 3
train_images.shape: (60000, 28, 28)
train_images.dtype: uint8
所以,這里 train_images 是一個(gè)由 8 位整數(shù)組成的 3D 張量。更確切地說(shuō),它是 60 000個(gè)矩陣組成的數(shù)組,每個(gè)矩陣由 28×28 個(gè)整數(shù)組成。每個(gè)這樣的矩陣都是一張灰度圖像,元素取值范圍為 0~255。
我們用 Matplotlib 庫(kù)( Python 標(biāo)準(zhǔn)科學(xué)套件的一部分)來(lái)顯示這個(gè) 3D 張量中的第 4 個(gè)數(shù)字,
如圖 2-2 所示。
#2-8 顯示第 4 個(gè)數(shù)字
digit = train_images[4]
import matplotlib.pyplot as plt
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()
train_images[4]就是train數(shù)據(jù)庫(kù)中的第5張圖片(從0開(kāi)始計(jì)數(shù))

2.2.6 在 Numpy 中操作張量
在前面的例子中,我們使用語(yǔ)法 train_images[i] 來(lái) 選擇 沿著第一個(gè)軸的特定數(shù)字。
選擇張量的特定元素叫作張量切片(tensor slicing) 。
我們來(lái)看一下 Numpy 數(shù)組上的張量切片運(yùn)算。
下面這個(gè)例子選擇第 10~100 個(gè)數(shù)字(不包括第 100 個(gè)),并將其放在形狀為 (90, 28, 28) 的數(shù)組中。
my_slice = train_images[10:100]
print('my_slice.shape',my_slice.shape)
運(yùn)行結(jié)果:
my_slice.shape (90, 28, 28)
它等同于下面這個(gè)更復(fù)雜的寫法,給出了切片沿著每個(gè)張量軸的起始索引和結(jié)束索引。
注意, : 等同于選擇整個(gè)軸。
my_slice = train_images[10:100, :, :]
print('my_slice.shape2:',my_slice.shape)my_slice = train_images[10:100, 0:28, 0:28]
print('my_slice.shape3:',my_slice.shape)
my_slice.shape2: (90, 28, 28)
my_slice.shape3: (90, 28, 28)
一般來(lái)說(shuō),你 可以沿著每個(gè)張量軸在任意兩個(gè)索引之間進(jìn)行選擇 。例如,你可以在所有圖
像的右下角選出 14 像素× 14 像素的區(qū)域:
my_slice = train_images[:, 14:, 14:]
也可以使用負(fù)數(shù)索引。與 Python 列表中的負(fù)數(shù)索引類似,它表示與當(dāng)前軸終點(diǎn)的相對(duì)位置。
你可以在圖像中心裁剪出 14 像素× 14 像素的區(qū)域:
my_slice = train_images[:, 7:-7, 7:-7]
my_slice = train_images[:, 14:, 14:]
print('my_slice.shape4:',my_slice.shape)my_slice = train_images[:, 7:-7, 7:-7]
print('my_slice.shape5:',my_slice.shape)
my_slice.shape4: (60000, 14, 14)
my_slice.shape5: (60000, 14, 14)
2.2.7 數(shù)據(jù)批量的概念
通常來(lái)說(shuō), 深度學(xué)習(xí)中所有數(shù)據(jù)張量的第一個(gè)軸(0 軸,因?yàn)樗饕龔?0 開(kāi)始)都是樣本軸 ( samples axis ,有時(shí)也叫 樣本維度 )。
在 MNIST 的例子中,樣本就是數(shù)字圖像。
此外, 深度學(xué)習(xí)模型不會(huì)同時(shí)處理整個(gè)數(shù)據(jù)集,而是將數(shù)據(jù)拆分成小批量 。
具體來(lái)看,下面是 MNIST 數(shù)據(jù)集的一個(gè)批量,批量大小為 128 。
batch = train_images[:128]
然后是下一個(gè)批量。 batch = train_images[128:256]
然后是第 n 個(gè)批量。 batch = train_images[128 * n:128 * (n + 1)]
對(duì)于這種批量張量,第一個(gè)軸(0 軸)叫作 批量軸(batch axis )或 批量維度(batch dimension) 。
在使用 Keras 和其他深度學(xué)習(xí)庫(kù)時(shí),你會(huì)經(jīng)常遇到這個(gè)術(shù)語(yǔ)。
2.2.8 現(xiàn)實(shí)世界中的數(shù)據(jù)張量
我們用幾個(gè)你未來(lái)會(huì)遇到的示例來(lái)具體介紹數(shù)據(jù)張量。你需要處理的數(shù)據(jù)幾乎總是以下類別之一。
向量數(shù)據(jù): 2D 張量,形狀為 ( samples, features ) 。
時(shí)間序列數(shù)據(jù)或序列數(shù)據(jù) : 3D 張量,形狀為 (samples, timesteps, features) 。
圖像: 4D 張量,形狀為 (samples, height, width, channels ) 或 (samples, channels, height, width) 。
視頻: 5D 張量,形狀為 (samples, frames, height, width, channels) 或 (samples,
frames, channels, height, width)
2.2.9 向量數(shù)據(jù)
這是最常見(jiàn)的數(shù)據(jù)。對(duì)于這種數(shù)據(jù)集,每個(gè)數(shù)據(jù)點(diǎn)都被編碼為一個(gè)向量,因此一個(gè)數(shù)據(jù)批量就被編碼為 2D 張量(即向量組成的數(shù)組),其中第一個(gè)軸是 樣本軸 ,第二個(gè)軸是 特征軸 。
2D 張量,形狀為 (samples, features)
我們來(lái)看兩個(gè)例子。
人口統(tǒng)計(jì)數(shù)據(jù)集 ,其中包括每個(gè)人的年齡、郵編和收入。
每個(gè)人可以表示為包含 3 個(gè)值的向量,而整個(gè)數(shù)據(jù)集包含 100 000 個(gè)人,因此可以存儲(chǔ)在形狀為 (100000, 3) 的 2D 張量中。
文本文檔數(shù)據(jù)集 ,我們將每個(gè)文檔表示為每個(gè)單詞在其中出現(xiàn)的次數(shù)(字典中包含20 000 個(gè)常見(jiàn)單詞)。每個(gè)文檔可以被編碼為包含 20 000 個(gè)值的向量(每個(gè)值對(duì)應(yīng)于字典中每個(gè)單詞的出現(xiàn)次數(shù)),整個(gè)數(shù)據(jù)集包含 500 個(gè)文檔,因此可以存儲(chǔ)在形狀為 (500, 20000) 的張量中。
2.2.10 時(shí)間序列數(shù)據(jù)或序列數(shù)據(jù)
當(dāng)時(shí)間(或序列順序)對(duì)于數(shù)據(jù)很重要時(shí),應(yīng)該將數(shù)據(jù)存儲(chǔ)在帶有時(shí)間軸的 3D 張量中。
每個(gè)樣本可以被編碼為一個(gè)向量序列(即 2D 張量),因此一個(gè)數(shù)據(jù)批量就被編碼為一個(gè) 3D 張
量(見(jiàn)圖 2-3 )。
3D 張量,形狀為 (samples, timesteps, features)。
根據(jù)慣例,時(shí)間軸始終是第 2 個(gè)軸(索引為 1 的軸) 。我們來(lái)看幾個(gè)例子。
股票價(jià)格數(shù)據(jù)集。 每一分鐘,我們將股票的當(dāng)前價(jià)格、前一分鐘的最高價(jià)格和前一分鐘 的最低價(jià)格保存下來(lái)。因此每分鐘被編碼為一個(gè) 3D 向量,整個(gè)交易日被編碼為一個(gè)形 狀為 (390, 3) 的 2D 張量(一個(gè)交易日有 390 分鐘),而 250 天的數(shù)據(jù)則可以保存在一 個(gè)形狀為 (250, 390, 3) 的 3D 張量中。這里每個(gè)樣本是一天的股票數(shù)據(jù)。
推文數(shù)據(jù)集。 我們將每條推文編碼為 280 個(gè)字符組成的序列,而每個(gè)字符又來(lái)自于 128 個(gè)字符組成的字母表。在這種情況下,每個(gè)字符可以被編碼為 大小為 128 的二進(jìn)制向量 (只有在該字符對(duì)應(yīng)的索引位置取值為 1 ,其他元素都為 0)。那么每條推文可以被編碼為一個(gè)形狀為 (280, 128) 的 2D 張量,而包含 100 萬(wàn)條推文的數(shù)據(jù)集則可以存儲(chǔ)在一 個(gè)形狀為 (1000000, 280, 128) 的張量中。
2.2.11 圖像數(shù)據(jù)
圖像通常具有三個(gè)維度: 高度、寬度和顏色深度 。
雖然灰度圖像(比如 MNIST 數(shù)字圖像) 只有一個(gè)顏色通道 ,因此可以保存在 2D 張量中,但按照慣例,圖像張量始終都是 3D 張量, 灰度圖像的彩色通道只有一維 。
因此,如果圖像大小為 256 × 256 ,那么 128 張 灰度圖像組成的批量可以保存在一個(gè)形狀為 ( 128 , 256, 256, 1 ) 的張量中,而 128 張彩色圖像組成的批量則可以保存在一個(gè)形狀為 (128, 256, 256, 3 ) 的張量中(見(jiàn)圖 2-4 )。
圖像張量的形狀有兩種約定: 通道在后(channels-last)的約定(在 TensorFlow 中使用) 和
通道在前 ( channels-first )的約定(在 Theano 中使用)。
Google 的 TensorFlow 機(jī)器學(xué)習(xí)框架將顏色深度軸放在最后:(samples, height, width, color_depth)。
與此相反, Theano 將圖像深度軸放在批量軸之后: (samples, color_depth, height, width) 。如果采
用 Theano 約定,前面的兩個(gè)例子將變成 (128, 1, 256, 256) 和 (128, 3, 256, 256) 。
Keras 框架同時(shí)支持這兩種格式。
2.2.12 視頻數(shù)據(jù)
視頻數(shù)據(jù)是現(xiàn)實(shí)生活中需要用到 5D 張量的少數(shù)數(shù)據(jù)類型之一。
視頻可以看作一系列幀, 每一幀都是一張彩色圖像 。
由于每一幀都可以保存在一個(gè)形狀為 (height, width, color_ depth) 的 3D 張量中,因此一系列幀可以保存在一個(gè)形狀為 (frames, height, width, color_depth) 的 4D 張量 中,而不同視頻組成的批量則可以保存在一個(gè) 5D 張量中,其形狀為 (samples, frames, height, width, color_depth) 。
舉個(gè)例子,一個(gè)以每秒 4 幀采樣的 60 秒 YouTube 視頻片段,視頻尺寸為 144 × 256,這個(gè) 視頻共有 240 幀。 4 個(gè)這樣的視頻片段組成的批量將保存在形狀為 (4, 240, 144, 256, 3) 的張量中??偣灿? 106 168 320 個(gè)值!如果張量的數(shù)據(jù)類型( dtype )是 float32,每個(gè)值都是 32 位,那么這個(gè)張量共有 405MB。好大!你在現(xiàn)實(shí)生活中遇到的視頻要小得多,因?yàn)樗鼈儾灰?float32 格式存儲(chǔ),而且通常被大大壓縮,比如 MPEG 格式。
2.3 神經(jīng)網(wǎng)絡(luò)的“齒輪”:張量運(yùn)算
所有計(jì)算機(jī)程序最終都可以簡(jiǎn)化為二進(jìn)制輸入上的一些二進(jìn)制運(yùn)算 (AND、OR、NOR 等) ,
與此類似,深度神經(jīng)網(wǎng)絡(luò)學(xué)到的所有變換也都可以簡(jiǎn)化為數(shù)值數(shù)據(jù)張量上的一些張量運(yùn)算(tensor
operation) ,例如加上張量、乘以張量等。
在最開(kāi)始的例子中,我們通過(guò)疊加 Dense 層來(lái)構(gòu)建網(wǎng)絡(luò)。
Keras 層的實(shí)例如下所示。
keras.layers.Dense(512, activation='relu')
這個(gè)層可以理解為一個(gè)函數(shù),輸入一個(gè) 2D 張量,返回另一個(gè) 2D 張量,即輸入張量的新表示。具體而言,這個(gè)函數(shù)如下所示(其中 W 是一個(gè) 2D 張量, b 是一個(gè)向量,二者都是該層的 屬性)。
output = relu(dot(W, input) + b)
我們將上式拆開(kāi)來(lái)看。這里有三個(gè)張量運(yùn)算:輸入張量和張量 W 之間的 點(diǎn)積運(yùn)算(dot) 、
得到的 2D 張量與向量 b 之間的 加法運(yùn)算(+) 、最后的 relu 運(yùn)算 。
relu(x) 是 max(x, 0) 。
注意 雖然本節(jié)的內(nèi)容都是關(guān)于線性代數(shù)表達(dá)式,但你卻找不到任何數(shù)學(xué)符號(hào)。我發(fā)現(xiàn),對(duì)于 沒(méi)有數(shù)學(xué)背景的程序員來(lái)說(shuō),如果用簡(jiǎn)短的 Python 代碼而不是數(shù)學(xué)方程來(lái)表達(dá)數(shù)學(xué)概念, 他們將更容易掌握。所以我們自始至終將會(huì)使用 Numpy 代碼。
2.3.1 逐元素運(yùn)算
relu 運(yùn)算和加法都是 逐元素 ( element-wise )的運(yùn)算,即該運(yùn)算獨(dú)立地應(yīng)用于張量中的每
個(gè)元素,也就是說(shuō),這些運(yùn)算非常適合大規(guī)模并行實(shí)現(xiàn)( 向量化 實(shí)現(xiàn),這一術(shù)語(yǔ)來(lái)自于 1970 —
1990 年間 向量處理器 超級(jí)計(jì)算機(jī)架構(gòu))。如果你想對(duì)逐元素運(yùn)算編寫簡(jiǎn)單的 Python 實(shí)現(xiàn),那么
可以用 for 循環(huán)。下列代碼是對(duì)逐元素 relu 運(yùn)算的簡(jiǎn)單實(shí)現(xiàn)。
def naive_relu(x):# x 是一個(gè) Numpy 的 2D 張量assert len(x.shape) == 2# 避免覆蓋輸入張量x = x.copy()# x 是一個(gè) Numpy 的 2D 張量for i in range(x.shape[0]):for j in range(x.shape[1]):x[i, j] = max(x[i, j], 0)return x
對(duì)于加法采用同樣的實(shí)現(xiàn)方法。
def naive_add(x, y):assert len(x.shape) == 2assert x.shape == y.shapex = x.copy()for i in range(x.shape[0]):for j in range(x.shape[1]):x[i, j] += y[i, j]return x
根據(jù)同樣的方法,你可以實(shí)現(xiàn)逐元素的乘法、減法等。
在實(shí)踐中處理 Numpy 數(shù)組時(shí),這些運(yùn)算都是優(yōu)化好的 Numpy 內(nèi)置函數(shù),這些函數(shù)將大量
運(yùn)算交給安裝好的基礎(chǔ)線性代數(shù)子程序( BLAS , basic linear algebra subprograms )實(shí)現(xiàn)(沒(méi)裝
的話,應(yīng)該裝一個(gè))。 BLAS 是低層次的、高度并行的、高效的張量操作程序,通常用 Fortran
或 C 語(yǔ)言來(lái)實(shí)現(xiàn)。
因此, 在 Numpy 中可以直接進(jìn)行下列逐元素運(yùn)算,速度非???/span> 。
import numpy as np
z = x + y
z = np.maximum(z, 0.)
2.3.2 廣播
上一節(jié) naive_add 的簡(jiǎn)單實(shí)現(xiàn)僅支持兩個(gè)形狀相同的 2D 張量相加。但在前面介紹的Dense 層中,我們將一個(gè) 2D 張量與一個(gè)向量相加。
如果將兩個(gè)形狀不同的張量相加,會(huì)發(fā)生什么?
如果沒(méi)有歧義的話, 較小的張量會(huì)被廣播(broadcast) ,以匹配較大張量的形狀。
廣播包含以下兩步。
(1) 向較小的張量添加軸(叫作廣播軸),使其 ndim 與較大的張量相同。
(2) 將較小的張量沿著新軸重復(fù),使其形狀與較大的張量相同。
來(lái)看一個(gè)具體的例子。
假設(shè) X 的形狀是 (32, 10) , y 的形狀是 (10,) 。首先,我們給 y 添加空的第一個(gè)軸,這樣 y 的形狀變?yōu)? (1, 10) 。然后,我們將 y 沿著新軸重復(fù) 32 次,這樣 得到的張量 Y 的形狀為 (32, 10) ,并且 Y[i, :] == y for i in range(0, 32)。現(xiàn)在, 我們可以將 X 和 Y 相加,因?yàn)樗鼈兊男螤钕嗤?
在實(shí)際的實(shí)現(xiàn)過(guò)程中并不會(huì)創(chuàng)建新的 2D 張量,因?yàn)槟菢幼龇浅5托?。重?fù)的操作完全是虛擬的,它只出現(xiàn)在算法中,而沒(méi)有發(fā)生在內(nèi)存中。但想象將向量沿著新軸重復(fù) 10 次,是一種很有用的思維模型。下面是一種簡(jiǎn)單的實(shí)現(xiàn)。
def naive_add_matrix_and_vector(x, y):# x 是一個(gè) Numpy 的 2D 張量assert len(x.shape) == 2# y 是一個(gè) Numpy 向量assert len(y.shape) == 1assert x.shape[1] == y.shape[0]# 避免覆蓋輸入張量x = x.copy()for i in range(x.shape[0]):for j in range(x.shape[1]):x[i, j] += y[j]return x
如果一個(gè)張量的形狀是 (a, b, ... n, n+1, ... m) ,另一個(gè)張量的形狀是 (n, n+1, ... m),那么你通常可以利用廣播對(duì)它們做兩個(gè)張量之間的逐元素運(yùn)算。廣播操作會(huì)自動(dòng)應(yīng)用 于從 a 到 n-1 的軸。
下面這個(gè)例子利用廣播將逐元素的 maximum 運(yùn)算 應(yīng)用于兩個(gè)形狀不同的張量。
import numpy as np
# x 是形狀為 (64, 3, 32, 10) 的隨機(jī)張量
x = np.random.random((64, 3, 32, 10))
# y 是形狀為 (32, 10) 的隨機(jī)張量
y = np.random.random((32, 10))
# 輸出 z 的形狀是 (64, 3, 32, 10),與 x 相同
z = np.maximum(x, y)
2.3.3 張量點(diǎn)積
點(diǎn)積運(yùn)算(dot product),也叫張量積 ( tensor product,不要與逐元素的乘積弄混),是最常見(jiàn)也最有用的張量運(yùn)算。與逐元素的運(yùn)算不同,它將輸入張量的元素合并在一起。
在 Numpy 、 Keras 、 Theano 和 TensorFlow 中,都是用 * 實(shí)現(xiàn)逐元素乘積。 TensorFlow 中的
點(diǎn)積使用了不同的語(yǔ)法 ,但在 Numpy 和 Keras 中,都是用標(biāo)準(zhǔn)的 dot 運(yùn)算符來(lái)實(shí)現(xiàn)點(diǎn)積。
import numpy as np
z = np.dot(x, y)
數(shù)學(xué)符號(hào)中的點(diǎn)( . )表示點(diǎn)積運(yùn)算。
z=x.y
從數(shù)學(xué)的角度來(lái)看,點(diǎn)積運(yùn)算做了什么?我們首先看一下兩個(gè)向量 x 和 y 的點(diǎn)積。其計(jì)算
過(guò)程如下。
def naive_vector_dot(x, y):# x 和 y 都是 Numpy 向量assert len(x.shape) == 1assert len(y.shape) == 1assert x.shape[0] == y.shape[0]z = 0.for i in range(x.shape[0]):z += x[i] * y[i]return z
注意, 兩個(gè)向量之間的點(diǎn)積是一個(gè)標(biāo)量 , 而且只有元素個(gè)數(shù)相同的向量之間才能做點(diǎn)積 。
你還可以對(duì)一個(gè)矩陣 x 和一個(gè)向量 y 做點(diǎn)積,返回值是一個(gè)向量,其中每個(gè)元素是 y 和 x
的每一行之間的點(diǎn)積。其實(shí)現(xiàn)過(guò)程如下。
def naive_matrix_vector_dot(x, y):# x 是一個(gè) Numpy 矩陣assert len(x.shape) == 2# y 是一個(gè) Numpy 向量assert len(y.shape) == 1# x 的第 1 維和 y 的第 0 維大小必須相同assert x.shape[1] == y.shape[0]# 這個(gè)運(yùn)算返回一個(gè)全是 0 的向量,# 其形狀與 x.shape[0] 相同z = np.zeros(x.shape[0])for i in range(x.shape[0]):for j in range(x.shape[1]):z[i] += x[i, j] * y[j]return z
你還可以復(fù)用前面寫過(guò)的代碼,從中可以看出矩陣 - 向量點(diǎn)積與向量點(diǎn)積之間的關(guān)系。
def naive_matrix_vector_dot(x, y):z = np.zeros(x.shape[0])for i in range(x.shape[0]):z[i] = naive_vector_dot(x[i, :], y)return z
注意,如果兩個(gè)張量中有一個(gè)的 ndim 大于 1,那么 dot 運(yùn)算就不再是對(duì)稱的,也就是說(shuō),dot(x, y) 不等于 dot(y, x)。
當(dāng)然,點(diǎn)積可以推廣到具有任意個(gè)軸的張量。最常見(jiàn)的應(yīng)用可能就是兩個(gè)矩陣之間的點(diǎn)積。
對(duì)于兩個(gè)矩陣 x 和 y,當(dāng)且僅當(dāng) x.shape[1] == y.shape[0] 時(shí),你才可以對(duì)它們做點(diǎn)積
(dot(x, y)) 。 得到的結(jié)果是一個(gè)形狀為 (x.shape[0], y.shape[1]) 的矩陣 ,其元素為 x
的行與 y 的列之間的點(diǎn)積。其簡(jiǎn)單實(shí)現(xiàn)如下。
def naive_matrix_dot(x, y):# x 和 y 都 是Numpy矩陣,二維張量assert len(x.shape) == 2assert len(y.shape) == 2# x 的第 1 維和 y 的第 0 維大小必須相同assert x.shape[1] == y.shape[0]# 這個(gè)運(yùn)算返回特定形狀的零矩陣z = np.zeros((x.shape[0], y.shape[1]))# 遍歷 x 的所有行……for i in range(x.shape[0]):# 然后遍歷 y 的所有列for j in range(y.shape[1]):row_x = x[i, :]column_y = y[:, j]z[i, j] = naive_vector_dot(row_x, column_y)return z
為了便于理解點(diǎn)積的形狀匹配,可以將輸入張量和輸出張量像圖 2-5 中那樣排列,利用可
視化來(lái)幫助理解。
圖 2-5 中, x 、 y 和 z 都用矩形表示(元素按矩形排列)。 x 的行和 y 的列必須大小相同,因 此 x 的寬度一定等于 y 的高度。 如果你打算開(kāi)發(fā)新的機(jī)器學(xué)習(xí)算法,可能經(jīng)常要畫這種圖 。
更一般地說(shuō),你可以對(duì)更高維的張量做點(diǎn)積,只要其形狀匹配遵循與前面 2D 張量相同的 原則:
(a, b, c, d) . (d,) -> (a, b, c)
(a, b, c, d) . (d, e) -> (a, b, c, e)
以此類推。
2.3.4 張量變形
第三個(gè)重要的張量運(yùn)算是 張量變形(tensor reshaping )。雖然前面神經(jīng)網(wǎng)絡(luò)第一個(gè)例子的 Dense 層中沒(méi)有用到它,但在 將圖像數(shù)據(jù)輸入神經(jīng)網(wǎng)絡(luò)之前 ,我們?cè)陬A(yù)處理時(shí)用到了這個(gè)運(yùn)算。
train_images = train_images.reshape((60000, 28 * 28))
張量變形是指改變張量的行和列,以得到想要的形狀 。變形后的張量的元素總個(gè)數(shù)與初始張量相同。簡(jiǎn)單的例子可以幫助我們理解張量變形。
import numpy as npx = np.array([[0., 1.],[2., 3.],[4., 5.]])
print('x=', x)
print(x.shape)x = x.reshape((6, 1))
print('x=', x)x = x.reshape((2, 3))
print('x=', x)
x= [[0. 1.][2. 3.][4. 5.]]
(3, 2)
x= [[0.][1.][2.][3.][4.][5.]]
x= [[0. 1. 2.][3. 4. 5.]]
經(jīng)常遇到的一種特殊的張量變形是 轉(zhuǎn)置(transposition) 。對(duì)矩陣做 轉(zhuǎn)置 是指將行和列互換,
使 x[i, :] 變?yōu)? x[:, i] 。
x = np.zeros((3, 4))
x = np.transpose(x)
print(x.shape)
(4, 3)
2.3.5 張量運(yùn)算的幾何解釋
對(duì)于張量運(yùn)算所操作的張量,其元素可以被解釋為某種幾何空間內(nèi)點(diǎn)的坐標(biāo),因此 所有的張量運(yùn)算都有幾何解釋 。舉個(gè)例子,我們來(lái)看加法。首先有這樣一個(gè)向量:
A = [0.5, 1]
它是二維空間中的一個(gè)點(diǎn)(見(jiàn)圖 2-6 )。常見(jiàn)的做法是將向量描繪成原點(diǎn)到這個(gè)點(diǎn)的箭頭,
如圖 2-7 所示。
假設(shè)又有一個(gè)點(diǎn):
B = [1, 0.25] ,
將它與前面的 A 相加。從幾何上來(lái)看,這相當(dāng)于將兩個(gè)向量箭頭連在一起,得到的位置表示兩個(gè)向量之和對(duì)應(yīng)的向量(見(jiàn)圖 2-8 )。
通常來(lái)說(shuō), 仿射變換、旋轉(zhuǎn)、縮放 等基本的幾何操作都可以表示為張量運(yùn)算。
舉個(gè)例子,要將一個(gè)二維向量旋轉(zhuǎn) theta 角,可以通過(guò)與一個(gè) 2 × 2 矩陣做點(diǎn)積來(lái)實(shí)現(xiàn),這個(gè)矩陣為 R = [u, v],其中 u 和 v 都是平面向量: u = [cos(theta), sin(theta)] , v = [-sin(theta), cos(theta)] 。
2.3.6 深度學(xué)習(xí)的幾何解釋
前面講過(guò),神經(jīng)網(wǎng)絡(luò)完全由一系列張量運(yùn)算組成,而這些張量運(yùn)算都只是輸入數(shù)據(jù)的幾何變換。
因此,你可以將神經(jīng)網(wǎng)絡(luò)解釋為高維空間中非常復(fù)雜的幾何變換,這種變換可以通過(guò)許多簡(jiǎn)單的步驟來(lái)實(shí)現(xiàn)。
對(duì)于三維的情況,下面這個(gè)思維圖像是很有用的。想象有兩張彩紙:一張紅色,一張藍(lán)色。其中一張紙放在另一張上?,F(xiàn)在將兩張紙一起揉成小球。這個(gè)皺巴巴的紙球就是你的輸入數(shù)據(jù),每張紙對(duì)應(yīng)于分類問(wèn)題中的一個(gè)類別。 神經(jīng)網(wǎng)絡(luò)(或者任何機(jī)器學(xué)習(xí)模型)要做的就是找 到可以讓紙球恢復(fù)平整的變換,從而能夠再次讓兩個(gè)類別明確可分 。通過(guò)深度學(xué)習(xí),這一過(guò)程 可以用三維空間中一系列簡(jiǎn)單的變換來(lái)實(shí)現(xiàn),比如你用手指對(duì)紙球做的變換,每次做一個(gè)動(dòng)作, 如圖 2-9 所示。
讓紙球恢復(fù)平整就是機(jī)器學(xué)習(xí)的內(nèi)容:為復(fù)雜的、高度折疊的數(shù)據(jù)流形找到簡(jiǎn)潔的表示 。
現(xiàn)在你應(yīng)該能夠很好地理解,為什么深度學(xué)習(xí)特別擅長(zhǎng)這一點(diǎn):它將復(fù)雜的幾何變換逐步分解為一長(zhǎng)串基本的幾何變換,這與人類展開(kāi)紙球所采取的策略大致相同。深度網(wǎng)絡(luò)的每一層都通過(guò)變換使數(shù)據(jù)解開(kāi)一點(diǎn)點(diǎn)——許多層堆疊在一起,可以實(shí)現(xiàn)非常復(fù)雜的解開(kāi)過(guò)程。
2.4 神經(jīng)網(wǎng)絡(luò)的“引擎”:基于梯度的優(yōu)化
上一節(jié)介紹過(guò),我們的第一個(gè)神經(jīng)網(wǎng)絡(luò)示例中,每個(gè)神經(jīng)層都用下述方法對(duì)輸入數(shù)據(jù)進(jìn)行變換。
output = relu(dot(W, input) + b)
在這個(gè)表達(dá)式中, W 和 b 都是張量,均為該層的屬性。它們被稱為該層的 權(quán)重(weight) 或 可訓(xùn)練參數(shù)(trainable parameter) ,分別對(duì)應(yīng) kernel 和 bias 屬性 。這些權(quán)重包含網(wǎng)絡(luò)從觀察訓(xùn)練數(shù)據(jù)中學(xué)到的信息。
一開(kāi)始,這些權(quán)重矩陣取較小的隨機(jī)值,這一步叫作 隨機(jī)初始化(random initialization) 。 當(dāng)然, W 和 b 都是隨機(jī)的, relu(dot(W, input) + b) 肯定不會(huì)得到任何有用的表示。雖然得到的表示是沒(méi)有意義的,但這是一個(gè)起點(diǎn)。
下一步則是根據(jù)反饋信號(hào)逐漸調(diào)節(jié)這些權(quán)重。這個(gè)逐漸調(diào)節(jié)的過(guò)程叫作 訓(xùn)練 ,也就是機(jī)器學(xué)習(xí)中的 學(xué)習(xí) 。
上述過(guò)程發(fā)生在一個(gè) 訓(xùn)練循環(huán)(training loop) 內(nèi),其具體過(guò)程如下。必要時(shí)一直重復(fù)這些 步驟。
(1) 抽取訓(xùn)練樣本 x 和對(duì)應(yīng)目標(biāo) y 組成的數(shù)據(jù)批量。
(2) 在 x 上運(yùn)行網(wǎng)絡(luò)[這一步叫作前向傳播(forward pass)],得到預(yù)測(cè)值 y_pred。
(3) 計(jì)算網(wǎng)絡(luò)在這批數(shù)據(jù)上的損失,用于衡量 y_pred 和 y 之間的距離。
(4) 更新網(wǎng)絡(luò)的所有權(quán)重,使網(wǎng)絡(luò)在這批數(shù)據(jù)上的損失略微下降。
最終得到的網(wǎng)絡(luò)在訓(xùn)練數(shù)據(jù)上的損失非常小,即預(yù)測(cè)值 y_pred 和預(yù)期目標(biāo) y 之間的距離非常小。網(wǎng)絡(luò)“學(xué)會(huì)”將輸入映射到正確的目標(biāo)。乍一看可能像魔法一樣,但如果你將其簡(jiǎn)化為基本步驟,那么會(huì)變得非常簡(jiǎn)單。
第一步看起來(lái)非常簡(jiǎn)單,只是輸入 / 輸出(I/O)的代碼。第二步和第三步僅僅是一些張量運(yùn)算的應(yīng)用,所以你完全可以利用上一節(jié)學(xué)到的知識(shí)來(lái)實(shí)現(xiàn)這兩步。 難點(diǎn)在于第四步:更新網(wǎng)絡(luò)的權(quán)重 ??紤]網(wǎng)絡(luò)中某個(gè)權(quán)重系數(shù),你怎么知道這個(gè)系數(shù)應(yīng)該增大還是減小,以及變化多少?
一種簡(jiǎn)單的解決方案是,保持網(wǎng)絡(luò)中其他權(quán)重不變,只考慮某個(gè)標(biāo)量系數(shù),讓其嘗試不同的取值。假設(shè)這個(gè)系數(shù)的初始值為 0.3。對(duì)一批數(shù)據(jù)做完前向傳播后,網(wǎng)絡(luò)在這批數(shù)據(jù)上的損失是 0.5 。如果你將這個(gè)系數(shù)的值改為 0.35 并重新運(yùn)行前向傳播,損失會(huì)增大到 0.6。但如果你將這個(gè)系數(shù)減小到 0.25 ,損失會(huì)減小到 0.4 。在這個(gè)例子中,將這個(gè)系數(shù)減小 0.05 似乎有助于使損失最小化。對(duì)于網(wǎng)絡(luò)中的所有系數(shù)都要重復(fù)這一過(guò)程。
但這種方法是非常低效的,因?yàn)閷?duì)每個(gè)系數(shù)(系數(shù)很多,通常有上千個(gè),有時(shí)甚至多達(dá)上百萬(wàn)個(gè))都需要計(jì)算兩次前向傳播(計(jì)算代價(jià)很大)。
一種更好的方法 是利用網(wǎng)絡(luò)中所有運(yùn)算都是 可微(differentiable) 的這一事實(shí),計(jì)算損失相對(duì)于網(wǎng)絡(luò)系數(shù)的 梯度(gradient) ,然后向梯度的反方向改變系數(shù),從而使損失降低。
如果你已經(jīng)了解 可微 和 梯度 這兩個(gè)概念,可以直接跳到 2.4.3 節(jié)。如果不了解,下面兩小節(jié)有助于你理解這些概念。
2.4.1 什么是導(dǎo)數(shù)
設(shè)有一個(gè)連續(xù)的光滑函數(shù) f(x) = y ,將實(shí)數(shù) x 映射為另一個(gè)實(shí)數(shù) y 。由于函數(shù)是 連續(xù)的微小變化只能導(dǎo)致 y 的微小變化——這就是函數(shù)連續(xù)性的直觀解釋。
假設(shè) x 增大了一個(gè)很小的因子 epsilon_x ,這導(dǎo)致 y 也發(fā)生了很小的變化,即 epsilon_y :
f(x + epsilon_x) = y + epsilon_y
此外,由于函數(shù)是 光滑的 (即函數(shù)曲線沒(méi)有突變的角度),在某個(gè)點(diǎn) p 附近,如果 epsilon_x
足夠小,就可以將 f 近似為斜率為 a 的線性函數(shù),這樣 epsilon_y 就變成了 a * epsilon_x :
f(x + epsilon_x) = y + a * epsilon_x
顯然,只有在 x 足夠接近 p 時(shí),這個(gè)線性近似才有效。
斜率 a 被稱為 f 在 p 點(diǎn)的導(dǎo)數(shù)(derivative )。如果 a 是負(fù)的,說(shuō)明 x 在 p 點(diǎn)附近的微小變化將導(dǎo)致 f(x) 減小(如圖 2-10 所示);如果 a 是正的,那么 x 的微小變化將導(dǎo)致 f(x) 增大。此外, a 的絕對(duì)值(導(dǎo)數(shù)大小)表示增大或減小的速度快慢。
對(duì)于每個(gè)可微函數(shù) f(x) ( 可微的意思是“可以被求導(dǎo)”。例如,光滑的連續(xù)函數(shù)可以被求導(dǎo)),都存在一個(gè)導(dǎo)數(shù)函數(shù) f '(x) ,將 x 的值映射為 f 在該點(diǎn)的局部線性近似的斜率。例如,cos(x)的導(dǎo)數(shù)是 -sin(x) , f(x) = a * x 的導(dǎo)數(shù)是 f'(x) = a ,等等。
如果你想要將 x 改變一個(gè)小因子 epsilon_x ,目的是將 f(x) 最小化,并且知道 f 的導(dǎo)數(shù),那么問(wèn)題解決了:導(dǎo)數(shù)完全描述了改變 x 后 f(x) 會(huì)如何變化。如果你希望減小 f(x) 的值,只需將 x 沿著導(dǎo)數(shù)的反方向移動(dòng)一小步。
2.4.2 張量運(yùn)算的導(dǎo)數(shù):梯度
梯度(gradient)是張量運(yùn)算的導(dǎo)數(shù) 。它是導(dǎo)數(shù)這一概念向多元函數(shù)導(dǎo)數(shù)的推廣。多元函數(shù)是以張量作為輸入的函數(shù)。
假設(shè)有一個(gè)輸入向量 x 、一個(gè)矩陣 W 、一個(gè)目標(biāo) y 和一個(gè)損失函數(shù) loss 。你可以用 W 來(lái)計(jì)算預(yù)測(cè)值 y_pred ,然后計(jì)算損失,或者說(shuō)預(yù)測(cè)值 y_pred 和目標(biāo) y 之間的距離。
y_pred = dot(W, x)
loss_value = loss(y_pred, y)
如果輸入數(shù)據(jù) x 和 y 保持不變,那么這可以看作將 W 映射到損失值的函數(shù)。
loss_value = f(W)
假設(shè) W 的當(dāng)前值為 W0 。 f 在 W0 點(diǎn)的導(dǎo)數(shù)是一個(gè)張量 gradient(f)(W0) ,其形狀與 W 相同,
每個(gè)系數(shù) gradient(f)(W0)[i, j] 表示改變 W0[i, j] 時(shí) loss_value 變化的方向和大小。
張量 gradient(f)(W0) 是函數(shù) f(W) = loss_value 在 W0 的導(dǎo)數(shù)。
前面已經(jīng)看到,單變量函數(shù) f(x) 的導(dǎo)數(shù)可以看作函數(shù) f 曲線的斜率。同樣, gradient(f) (W0) 也可以看作表示 f(W) 在 W0 附近曲率(curvature)的張量 。
對(duì)于一個(gè)函數(shù) f(x) ,你可以通過(guò)將 x 向?qū)?shù)的反方向移動(dòng)一小步來(lái)減小 f(x) 的值。同樣,對(duì)于張量的函數(shù) f(W) ,你也可以通過(guò)將 W 向梯度的反方向移動(dòng)來(lái)減小 f(W) ,比如 W1 =W0 - step * gradient(f)(W0) ,其中 step 是一個(gè)很小的比例因子。也就是說(shuō),沿著曲率的反方向移動(dòng),直觀上來(lái)看在曲線上的位置會(huì)更低。注意,比例因子 step 是必需的,因?yàn)間radient(f)(W0) 只是 W0 附近曲率的近似值,不能離 W0 太遠(yuǎn)。
2.4.3 隨機(jī)梯度下降
給定一個(gè)可微函數(shù),理論上可以用解析法找到它的最小值:函數(shù)的最小值是導(dǎo)數(shù)為 0 的點(diǎn),
因此你只需找到所有導(dǎo)數(shù)為 0 的點(diǎn),然后計(jì)算函數(shù)在其中哪個(gè)點(diǎn)具有最小值。
將這一方法應(yīng)用于神經(jīng)網(wǎng)絡(luò),就是用解析法求出最小損失函數(shù)對(duì)應(yīng)的所有權(quán)重值。可以通
過(guò)對(duì)方程 gradient(f)(W) = 0 求解 W 來(lái)實(shí)現(xiàn)這一方法。這是包含 N 個(gè)變量的多項(xiàng)式方程,
其中 N 是網(wǎng)絡(luò)中系數(shù)的個(gè)數(shù)。 N=2 或 N=3 時(shí)可以對(duì)這樣的方程求解,但對(duì)于實(shí)際的神經(jīng)網(wǎng)絡(luò)是
無(wú)法求解的,因?yàn)閰?shù)的個(gè)數(shù)不會(huì)少于幾千個(gè),而且經(jīng)常有上千萬(wàn)個(gè)。
相反,你可以使用 2.4 節(jié)開(kāi)頭總結(jié)的四步算法:基于當(dāng)前在隨機(jī)數(shù)據(jù)批量上的損失,一點(diǎn)一點(diǎn)地對(duì)參數(shù)進(jìn)行調(diào)節(jié)。由于處理的是一個(gè)可微函數(shù),你可以計(jì)算出它的梯度,從而有效地實(shí)現(xiàn)第四步。沿著梯度的反方向更新權(quán)重,損失每次都會(huì)變小一點(diǎn)。
(1) 抽取訓(xùn)練樣本 x 和對(duì)應(yīng)目標(biāo) y 組成的數(shù)據(jù)批量。
(2) 在 x 上運(yùn)行網(wǎng)絡(luò),得到預(yù)測(cè)值 y_pred。
(3) 計(jì)算網(wǎng)絡(luò)在這批數(shù)據(jù)上的損失,用于衡量 y_pred 和 y 之間的距離。
(4) 計(jì)算損失相對(duì)于網(wǎng)絡(luò)參數(shù)的梯度[一次反向傳播(backward pass)]。
(5) 將參數(shù)沿著梯度的反方向移動(dòng)一點(diǎn),比如 W -= step * gradient,從而使這批數(shù)據(jù)上的損失減小一點(diǎn)。
這很簡(jiǎn)單!我剛剛描述的方法叫作 小批量隨機(jī)梯度下降(mini-batch stochastic gradient descent, 又稱為小批量 SGD) 。術(shù)語(yǔ) 隨機(jī) ( stochastic )是指每批數(shù)據(jù)都是隨機(jī)抽取的( stochastic 是 random 在科學(xué)上的同義詞 a )。圖 2-11 給出了一維的情況,網(wǎng)絡(luò)只有一個(gè)參數(shù),并且只有一個(gè)訓(xùn)練樣本。
如你所見(jiàn),直觀上來(lái)看,為 step 因子選取合適的值是很重要的。如果取值太小,則沿著
曲線的下降需要很多次迭代,而且可能會(huì)陷入局部極小點(diǎn)。如果取值太大,則更新權(quán)重值之后
可能會(huì)出現(xiàn)在曲線上完全隨機(jī)的位置。
注意,小批量 SGD 算法的一個(gè)變體是每次迭代時(shí)只抽取一個(gè)樣本和目標(biāo),而不是抽取一批
數(shù)據(jù)。這叫作 真 SGD (有別于 小批量 SGD )。還有另一種極端,每一次迭代都在 所有 數(shù)據(jù)上
運(yùn)行,這叫作 批量 SGD 。這樣做的話,每次更新都更加準(zhǔn)確,但計(jì)算代價(jià)也高得多。這兩個(gè)極
端之間的有效折中則是選擇合理的批量大小。
圖 2-11 描述的是一維參數(shù)空間中的梯度下降,但在實(shí)踐中需要在高維空間中使用梯度下降。神經(jīng)網(wǎng)絡(luò)的每一個(gè)權(quán)重參數(shù)都是空間中的一個(gè)自由維度,網(wǎng)絡(luò)中可能包含數(shù)萬(wàn)個(gè)甚至上百萬(wàn)個(gè)參數(shù)維度。為了讓你對(duì)損失曲面有更直觀的認(rèn)識(shí),你還可以將梯度下降沿著二維損失曲面可視化,
如圖 2-12 所示 。但你不可能將神經(jīng)網(wǎng)絡(luò)的實(shí)際訓(xùn)練過(guò)程可視化,因?yàn)槟銦o(wú)法用人類可以理解的
方式來(lái)可視化 1 000 000 維空間。因此最好記住,在這些低維表示中形成的直覺(jué)在實(shí)踐中不一定
總是準(zhǔn)確的。這在歷史上一直是深度學(xué)習(xí)研究的問(wèn)題來(lái)源。
2.4.4 鏈?zhǔn)角髮?dǎo):反向傳播算法
在前面的算法中,我們假設(shè)函數(shù)是可微的,因此可以明確計(jì)算其導(dǎo)數(shù)。在實(shí)踐中,神經(jīng)網(wǎng)
絡(luò)函數(shù)包含許多連接在一起的張量運(yùn)算,每個(gè)運(yùn)算都有簡(jiǎn)單的、已知的導(dǎo)數(shù)。例如,下面這個(gè)
網(wǎng)絡(luò) f 包含 3 個(gè)張量運(yùn)算 a 、 b 和 c ,還有 3 個(gè)權(quán)重矩陣 W1 、 W2 和 W3 。
f(W1, W2, W3) = a(W1, b(W2, c(W3)))
根據(jù)微積分的知識(shí),這種函數(shù)鏈可以利用下面這個(gè)恒等式進(jìn)行求導(dǎo),它稱為 鏈?zhǔn)椒▌t(chain rule ):
(f(g(x)))' = f'(g(x)) * g'(x)。
將鏈?zhǔn)椒▌t應(yīng)用于神經(jīng)網(wǎng)絡(luò)梯度值的計(jì)算,得到的算法叫作 反向傳播(backpropagation,有時(shí)也叫反式微分,reverse-mode differentiation) 。反向傳播從最終損失值開(kāi)始,從最頂層反向作用至最底層,利用鏈?zhǔn)椒▌t計(jì)算每個(gè)參數(shù)對(duì)損失值的貢獻(xiàn)大小。
2.5 回顧第一個(gè)例子
你已經(jīng)讀到了本章最后一節(jié),現(xiàn)在應(yīng)該對(duì)神經(jīng)網(wǎng)絡(luò)背后的原理有了大致的了解。我們回頭
看一下第一個(gè)例子,并根據(jù)前面三節(jié)學(xué)到的內(nèi)容來(lái)重新閱讀這個(gè)例子中的每一段代碼。
下面是輸入數(shù)據(jù)。
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
現(xiàn)在你明白了,輸入圖像保存在 float32 格式的 Numpy 張量中,形狀分別為 (60000, 784) (訓(xùn)練數(shù)據(jù))和 (10000, 784) (測(cè)試數(shù)據(jù))。
下面是構(gòu)建網(wǎng)絡(luò)。
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
現(xiàn)在你明白了,這個(gè)網(wǎng)絡(luò)包含 兩個(gè) Dense 層 ,每層都對(duì)輸入數(shù)據(jù)進(jìn)行一些簡(jiǎn)單的張量運(yùn)算,這些運(yùn)算都包含權(quán)重張量。權(quán)重張量是該層的屬性,里面保存了網(wǎng)絡(luò)所學(xué)到的 知識(shí) ( knowledge )。
下面是網(wǎng)絡(luò)的編譯。
network.compile(optimizer='rmsprop',loss='categorical_crossentropy',metrics=['accuracy'])
現(xiàn)在你明白了, categorical_crossentropy 是損失函數(shù) ,是用于學(xué)習(xí)權(quán)重張量的反饋信號(hào),在訓(xùn)練階段應(yīng)使它最小化。你還知道,減小損失是通過(guò)小批量隨機(jī)梯度下降來(lái)實(shí)現(xiàn)的。
梯度下降的具體方法由第一個(gè)參數(shù)給定,即 rmsprop 優(yōu)化器 。
最后,下面是訓(xùn)練循環(huán)。
network.fit(train_images, train_labels, epochs=5, batch_size=128)
現(xiàn)在你明白在調(diào)用 fit 時(shí)發(fā)生了什么:網(wǎng)絡(luò)開(kāi)始在訓(xùn)練數(shù)據(jù)上進(jìn)行迭代( 每個(gè)小批量包含128 個(gè)樣本 ),共 迭代 5 次 [在所有訓(xùn)練數(shù)據(jù)上迭代一次叫作一個(gè) 輪次 ( epoch)]。在每次迭代過(guò)程中,網(wǎng)絡(luò)會(huì)計(jì)算批量損失相對(duì)于權(quán)重的梯度,并相應(yīng)地更新權(quán)重。 5 輪之后,網(wǎng)絡(luò)進(jìn)行了2345 次梯度更新(每輪 469 次),網(wǎng)絡(luò)損失值將變得足夠小,使得網(wǎng)絡(luò)能夠以很高的精度對(duì)手
寫數(shù)字進(jìn)行分類。
到目前為止,你已經(jīng)了解了神經(jīng)網(wǎng)絡(luò)的大部分知識(shí)。
本章小結(jié)
學(xué)習(xí)是指找到一組模型參數(shù),使得在給定的訓(xùn)練數(shù)據(jù)樣本和對(duì)應(yīng)目標(biāo)值上的損失函數(shù)最小化 。
學(xué)習(xí)的過(guò)程:隨機(jī)選取包含數(shù)據(jù)樣本及其目標(biāo)值的批量,并計(jì)算批量損失相對(duì)于網(wǎng)絡(luò)參 數(shù)的梯度。隨后將網(wǎng)絡(luò)參數(shù)沿著梯度的反方向稍稍移動(dòng)(移動(dòng)距離由學(xué)習(xí)率指定)。
整個(gè)學(xué)習(xí)過(guò)程之所以能夠?qū)崿F(xiàn),是因?yàn)樯窠?jīng)網(wǎng)絡(luò)是一系列可微分的張量運(yùn)算,因此可以利用求導(dǎo)的鏈?zhǔn)椒▌t來(lái)得到梯度函數(shù),這個(gè)函數(shù)將當(dāng)前參數(shù)和當(dāng)前數(shù)據(jù)批量映射為一個(gè)梯度值。
后續(xù)幾章你會(huì)經(jīng)常遇到兩個(gè)關(guān)鍵的概念: 損失和優(yōu)化器 。將數(shù)據(jù)輸入網(wǎng)絡(luò)之前,你需要先定義這二者。
損失是在訓(xùn)練過(guò)程中需要最小化的量,因此,它應(yīng)該能夠衡量當(dāng)前任務(wù)是否已成功解決。
優(yōu)化器是使用損失梯度更新參數(shù)的具體方式 ,比如 RMSProp 優(yōu)化器 、帶動(dòng)量的隨機(jī)梯度下降( SGD )等。