Deep Learning with Python 讀書筆記 6.27 I 神經網絡的數學基礎

我感覺這樣記錄,對於我來說挺好的。因爲我看兩端對齊的語句容易走神,這樣記錄閱讀的話,就很少出現之前的情況。

我寫的初衷,也是自己來看,所以感覺寫的不好的,請保留下意見,謝謝。

代碼縮進情況,字體重複情況,因爲我能看懂,就沒改。

 

 

裏面的每一個字我都看過,加粗 括號  下劃線 等均是我的筆記。

 

要理解深度學習,需要熟悉很多簡單的數學概念:張量、張量運算、微分、梯度下降等。
本章目的是用不那麼技術化的文字幫你建立對這些概念的直覺。特別地,我們將避免使用數學
符號,因爲數學符號可能會令沒有任何數學背景的人反感,而且對解釋問題也不是絕對必要的。
本章將首先給出一個神經網絡的示例,引出張量和梯度下降的概念,然後逐個詳細介紹。
請記住,這些概念對於理解後續章節中的示例至關重要。
讀完本章後,你會對神經網絡的工作原理有一個直觀的理解,然後就可以學習神經網絡的
實際應用了(從第 3 章開始)。
2.1 初識神經網絡
我們來看一個具體的神經網絡示例,使用 Python Keras 庫來學習手寫數字分類。如果你
沒用過 Keras 或類似的庫,可能無法立刻搞懂這個例子中的全部內容。甚至你可能還沒有安裝
Keras。沒關係,下一章會詳細解釋這個例子中的每個步驟。因此,如果其中某些步驟看起來有
些隨意,或者像魔法一樣,也請你不要擔心。下面我們要開始了。
我們這裏要解決的問題是,將手寫數字的灰度圖像(28 像素×28 像素)劃分到 10 個類別
中(0~9)。我們將使用 MNIST 數據集,它是機器學習領域的一個經典數據集,其歷史幾乎和這
個領域一樣長,而且已被人們深入研究。這個數據集包含 60 000 張訓練圖像和 10 000 張測試圖
像,由美國國家標準與技術研究院(National Institute of Standards and Technology,即 MNIST
NIST)在 20 世紀 80 年代收集得到。你可以將“解決”MNIST 問題看作深度學習的“Hello
World”,正是用它來驗證你的算法是否按預期運行。當你成爲機器學習從業者後,會發現
MNIST 一次又一次地出現在科學論文、博客文章等中。圖 2-1 給出了 MNIST 數據集的一些樣本。
關於類和標籤的說明
在機器學習中,分類問題中的某個類別叫作類(class)。數據點叫作樣本(sample)。某
個樣本對應的類叫作標籤(label)。
你不需要現在就嘗試在計算機上運行這個例子。但如果你想這麼做的話,首先需要安裝
Keras,安裝方法見 3.3 節。
MNIST 數據集預先加載在 Keras 庫中,其中包括 4 Numpy 數組。
代碼清單 2-1 加載 Keras 中的 MNIST 數據集
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images train_labels 組成了訓練集training set),模型將從這些數據中進行
學習。然後在測試集test set,即 test_images test_labels)上對模型進行測試。
圖像被編碼爲 Numpy 數組,而標籤是數字數組,取值範圍爲 0~9。圖像和標籤一一對應。
我們來看一下訓練數據:
>>> train_images.shape
(60000, 28, 28)
>>> len(train_labels)
60000
>>> train_labels
array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)
下面是測試數據:
>>> test_images.shape
(10000, 28, 28)
>>> len(test_labels)
10000
>>> test_labels
array([7, 2, 1, ..., 4, 5, 6], dtype=uint8)
接下來的工作流程如下:首先,將訓練數據(train_images train_labels)輸入神
經網絡;其次,網絡學習將圖像和標籤關聯在一起;最後,網絡對 test_images 生成預測,
而我們將驗證這些預測與 test_labels 中的標籤是否匹配。
下面我們來構建網絡。再說一遍,你現在不需要理解這個例子的全部內容。
代碼清單 2-2 網絡架構
from keras import models
from keras import layers
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
神經網絡的核心組件是層(layer),它是一種數據處理模塊,你可以將它看成數據過濾器。
進去一些數據,出來的數據變得更加有用。具體來說,層從輸入數據中提取表示——我們期望
這種表示有助於解決手頭的問題。大多數深度學習都是將簡單的層鏈接起來,從而實現漸進式
數據蒸餾data distillation)。深度學習模型就像是數據處理的篩子,包含一系列越來越精細的
數據過濾器(即層)。
本例中的網絡包含 2 Dense 層,它們是密集連接(也叫全連接)的神經層。第二層(也
是最後一層)是一個 10 softmax 層,它將返回一個由 10 個概率值(總和爲 1)組成的數組。
每個概率值表示當前數字圖像屬於 10 個數字類別中某一個的概率。
要想訓練網絡,我們還需要選擇編譯compile)步驟的三個參數。
損失函數(loss function):網絡如何衡量在訓練數據上的性能,即網絡如何朝着正確的
方向前進。
優化器(optimizer):基於訓練數據和損失函數來更新網絡的機制。
在訓練和測試過程中需要監控的指標(metric):本例只關心精度,即正確分類的圖像所
佔的比例。
後續兩章會詳細解釋損失函數和優化器的確切用途。
代碼清單 2-3 編譯步驟
network.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
在開始訓練之前,我們將對數據進行預處理,將其變換爲網絡要求的形狀,並縮放到所
有值都在 [0, 1] 區間。比如,之前訓練圖像保存在一個 uint8 類型的數組中,其形狀爲
(60000, 28, 28),取值區間爲 [0, 255]。我們需要將其變換爲一個 float32 數組,其形
狀爲 (60000, 28 * 28),取值範圍爲 0~1
代碼清單 2-4 準備圖像數據
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
我們還需要對標籤進行分類編碼,第 3 章將會對這一步驟進行解釋。
代碼清單 2-5 準備標籤
from keras.utils import to_categorical
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
現在我們準備開始訓練網絡,在 Keras 中這一步是通過調用網絡的 fit 方法來完成的——
我們在訓練數據上擬合fit)模型。

>>> network.fit(train_images, train_labels, epochs=5, batch_size=128) 

 

epoch   

  • 英 /ˈiːpɒk/
  • 曆元
  • 時代

batch

n. 一批;一爐;一次所制之量

vt. 分批處理

Epoch 1/5
60000/60000 [=============================] - 9s - loss: 0.2524 - acc: 0.9273
Epoch 2/5
51328/60000 [=======================>.....] - ETA: 1s - loss: 0.1035 - acc: 0.9692
訓練過程中顯示了兩個數字:一個是網絡在訓練數據上的損失(loss),另一個是網絡在
訓練數據上的精度(acc)。
我們很快就在訓練數據上達到了 0.98998.9%)的精度。現在我們來檢查一下模型在測試
集上的性能。
>>> test_loss, test_acc = network.evaluate(test_images, test_labels)
>>> print('test_acc:', test_acc)
test_acc: 0.9785
測試集精度爲 97.8%,比訓練集精度低不少。訓練精度和測試精度之間的這種差距是過擬
overfit)造成的。過擬合是指機器學習模型在新數據上的性能往往比在訓練數據上要差,它
是第 3 章的核心主題。
第一個例子到這裏就結束了。你剛剛看到了如何構建和訓練一個神經網絡,用不到 20 行的
Python 代碼對手寫數字進行分類。下一章會詳細介紹這個例子中的每一個步驟,並講解其背後
的原理。接下來你將要學到張量(輸入網絡的數據存儲對象)、張量運算(層的組成要素)和梯
度下降(可以讓網絡從訓練樣本中進行學習)。
2.2 神經網絡的數據表示
前面例子使用的數據存儲在多維 Numpy 數組中也叫張量tensor。一般來說,當前所
有機器學習系統都使用張量作爲基本數據結構。張量對這個領域非常重要,重要到 Google
TensorFlow 都以它來命名。那麼什麼是張量?
張量這一概念的核心在於,它是一個數據容器。它包含的數據幾乎總是數值數據,因此它
是數字的容器。你可能對矩陣很熟悉,它是二維張量。張量是矩陣向任意維度的推廣[注意,
張量的維度dimension)通常叫作axis)]。

axis

英 /ˈæksɪs

美 /ˈæksɪs/

n. 軸;軸線;軸心國

2.2.1 標量(0D 張量)

scalar

英 /ˈskeɪlə(r)/

美 /ˈskeɪlər/

n. [數] 標量;[數] 數量

adj. 標量的;數量的;梯狀的,分等級的

僅包含一個數字的張量叫作標量scalar,也叫標量張量、零維張量、0D 張量)。在 Numpy
中,一個 float32 float64 的數字就是一個標量張量(或標量數組)。你可以用 ndim 屬性
來查看一個 Numpy 張量的軸的個數。標量張量有 0 個軸(ndim == 0)。張量軸的個數也叫作
rank)。下面是一個 Numpy 標量。
>>> import numpy as np
>>> x = np.array(12)
>>> x
array(12)
>>> x.ndim
0
2.2.2 向量(1D 張量)
數字組成的數組叫作向量vector)或一維張量(1D 張量)。一維張量只有一個軸。下面是
一個 Numpy 向量。
>>> x = np.array([12, 3, 6, 14, 7])
>>> x
array([12, 3, 6, 14, 7])
>>> x.ndim
1
這個向量有 5 個元素,所以被稱爲 5D 向量。不要把 5D 向量和 5D 張量弄混! 5D 向量只
有一個軸,沿着軸有 5 個維度,而 5D 張量有 5 個軸(沿着每個軸可能有任意個維度)。維度
dimensionality)可以表示沿着某個軸上的元素個數(比如 5D 向量),也可以表示張量中軸的個
數(比如 5D 張量),這有時會令人感到混亂。對於後一種情況,技術上更準確的說法是 5 階張量
(張量的階數即軸的個數),但 5D 張量這種模糊的寫法更常見。
2.2.3 矩陣(2D 張量)
向量組成的數組叫作矩陣matrix)或二維張量(2D 張量)。矩陣有 2 個軸(通常叫作
)。你可以將矩陣直觀地理解爲數字組成的矩形網格。下面是一個 Numpy 矩陣。
>>> x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
>>> x.ndim
2
第一個軸上的元素叫作row),第二個軸上的元素叫作column)。在上面的例子中,
[5, 78, 2, 34, 0] x 的第一行,[5, 6, 7] 是第一列。
2.2.4 3D 張量與更高維張量
將多個矩陣組合成一個新的數組,可以得到一個 3D 張量,你可以將其直觀地理解爲數字
組成的立方體。下面是一個 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
將多個 3D 張量組合成一個數組,可以創建一個 4D 張量,以此類推。深度學習處理的一般
0D 4D 的張量,但處理視頻數據時可能會遇到 5D 張量。
2.2.5 關鍵屬性
張量是由以下三個關鍵屬性來定義的。
軸的個數(階)。例如,3D 張量有 3 個軸,矩陣有 2 個軸。這在 Numpy 等 Python 庫中
也叫張量的 ndim。
形狀。這是一個整數元組,表示張量沿每個軸的維度大小(元素個數)。例如,前面矩
陣示例的形狀爲 (3, 5),3D 張量示例的形狀爲 (3, 3, 5)。向量的形狀只包含一個
元素,比如 (5,),而標量的形狀爲空,即 ()。
數據類型(在 Python 庫中通常叫作 dtype)。這是張量中所包含數據的類型,例如,張
量的類型可以是 float32、uint8、float64 等。在極少數情況下,你可能會遇到字符
(char)張量。注意,Numpy(以及大多數其他庫)中不存在字符串張量,因爲張量存
儲在預先分配的連續內存段中,而字符串的長度是可變的,無法用這種方式存儲。
爲了具體說明,我們回頭看一下 MNIST 例子中處理的數據。首先加載 MNIST 數據集。
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
接下來,我們給出張量 train_images 的軸的個數,即 ndim 屬性。
>>> print(train_images.ndim)
3
下面是它的形狀。
>>> print(train_images.shape)
(60000, 28, 28)
下面是它的數據類型,即 dtype 屬性。
>>> print(train_images.dtype)
uint8
所以,這裏 train_images 是一個由 8 位整數組成的 3D 張量。更確切地說,它是 60 000
個矩陣組成的數組,每個矩陣由 28×28 個整數組成。每個這樣的矩陣都是一張灰度圖像,元素
取值範圍爲 0~255
我們用 Matplotlib 庫(Python 標準科學套件的一部分)來顯示這個 3D 張量中的第 4 個數字,
如圖 2-2 所示。
代碼清單 2-6 顯示第 4 個數字
digit = train_images[4]
import matplotlib.pyplot as plt
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()
2.2.6 在 Numpy 中操作張量
在前面的例子中,我們使用語法 train_images[i] 選擇沿着第一個軸的特定數字。選
擇張量的特定元素叫作張量切片tensor slicing)。我們來看一下 Numpy 數組上的張量切片運算。
下面這個例子選擇第 10~100 個數字(不包括第 100 個),並將其放在形狀爲 (90, 28,
28) 的數組中。
>>> my_slice = train_images[10:100]
>>> print(my_slice.shape)
(90, 28, 28)
它等同於下面這個更復雜的寫法,給出了切片沿着每個張量軸的起始索引和結束索引。
注意,: 等同於選擇整個軸
一般來說,你可以沿着每個張量軸在任意兩個索引之間進行選擇。例如,你可以在所有圖
像的右下角選出 14 像素×14 像素的區域:
my_slice = train_images[:, 14:, 14:]
也可以使用負數索引。與 Python 列表中的負數索引類似,它表示與當前軸終點的相對位置。
你可以在圖像中心裁剪出 14 像素×14 像素的區域:
my_slice = train_images[:, 7:-7, 7:-7]
2.2.7 數據批量的概念
通常來說,深度學習中所有數據張量的第一個軸(0 軸,因爲索引從 0 開始)都是樣本軸
samples axis,有時也叫樣本維度)。在 MNIST 的例子中,樣本就是數字圖像。
此外,深度學習模型不會同時處理整個數據集,而是將數據拆分成小批量。具體來看,下
面是 MNIST 數據集的一個批量,批量大小爲 128
batch = train_images[:128]
然後是下一個批量。
batch = train_images[128:256]
然後是第 n 個批量。
batch = train_images[128 * n:128 * (n + 1)]
對於這種批量張量,第一個軸(0 軸)叫作批量軸batch axis)或批量維度batch dimension)。
在使用 Keras 和其他深度學習庫時,你會經常遇到這個術語。
2.2.8 現實世界中的數據張量
我們用幾個你未來會遇到的示例來具體介紹數據張量。你需要處理的數據幾乎總是以下類
別之一。
向量數據:2D 張量,形狀爲 (samples, features)。
時間序列數據或序列數據: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 向量數據
這是最常見的數據。對於這種數據集,每個數據點都被編碼爲一個向量,因此一個數據批
量就被編碼爲 2D 張量(即向量組成的數組),其中第一個軸是樣本軸,第二個軸是特徵軸
我們來看兩個例子。
人口統計數據集,其中包括每個人的年齡、郵編和收入。每個人可以表示爲包含 3 個值
的向量,而整個數據集包含 100 000 個人,因此可以存儲在形狀爲 (100000, 3) 2D
張量中。
文本文檔數據集,我們將每個文檔表示爲每個單詞在其中出現的次數(字典中包含
20 000 個常見單詞)。每個文檔可以被編碼爲包含 20 000 個值的向量(每個值對應於
字典中每個單詞的出現次數),整個數據集包含 500 個文檔,因此可以存儲在形狀爲
(500, 20000) 的張量中。
2.2.10 時間序列數據或序列數據
當時間(或序列順序)對於數據很重要時,應該將數據存儲在帶有時間軸的 3D 張量中。
每個樣本可以被編碼爲一個向量序列(即 2D 張量),因此一個數據批量就被編碼爲一個 3D
量(見圖 2-3)。
根據慣例,時間軸始終是第 2 個軸(索引爲 1 的軸)。我們來看幾個例子。
股票價格數據集。每一分鐘,我們將股票的當前價格、前一分鐘的最高價格和前一分鐘
的最低價格保存下來。因此每分鐘被編碼爲一個 3D 向量,整個交易日被編碼爲一個形
狀爲 (390, 3) 2D 張量(一個交易日有 390 分鐘),而 250 天的數據則可以保存在一
個形狀爲 (250, 390, 3) 3D 張量中。這裏每個樣本是一天的股票數據。
推文數據集。我們將每條推文編碼爲 280 個字符組成的序列,而每個字符又來自於 128
個字符組成的字母表。在這種情況下,每個字符可以被編碼爲大小爲 128 的二進制向量
(只有在該字符對應的索引位置取值爲 1,其他元素都爲 0)。那麼每條推文可以被編碼
爲一個形狀爲 (280, 128) 2D 張量,而包含 100 萬條推文的數據集則可以存儲在一
個形狀爲 (1000000, 280, 128) 的張量中。
2.2.11 圖像數據
圖像通常具有三個維度:高度、寬度和顏色深度。雖然灰度圖像(比如 MNIST 數字圖像)
只有一個顏色通道,因此可以保存在 2D 張量中,但按照慣例,圖像張量始終都是 3D 張量,灰
度圖像的彩色通道只有一維。因此,如果圖像大小爲 256×256,那麼 128 張灰度圖像組成的批
量可以保存在一個形狀爲 (128, 256, 256, 1) 的張量中,而 128 張彩色圖像組成的批量則可以保存在一個形狀爲 (128, 256, 256, 3) 的張量中(見圖 2-4)。
圖像張量的形狀有兩種約定:通道在後channels-last)的約定(在 TensorFlow 中使用)和
通道在前channels-first)的約定(在 Theano 中使用)。Google TensorFlow 機器學習框架將
顏色深度軸放在最後:(samples, height, width, color_depth)。與此相反,Theano
將圖像深度軸放在批量軸之後:(samples, color_depth, height, width)。如果採
Theano 約定,前面的兩個例子將變成 (128, 1, 256, 256) (128, 3, 256, 256)
Keras 框架同時支持這兩種格式。
2.2.12 視頻數據
視頻數據是現實生活中需要用到 5D 張量的少數數據類型之一。視頻可以看作一系列幀,
每一幀都是一張彩色圖像。由於每一幀都可以保存在一個形狀爲 (height, width, color_
depth) 3D 張量中,因此一系列幀可以保存在一個形狀爲 (frames, height, width,
color_depth) 4D 張量中,而不同視頻組成的批量則可以保存在一個 5D 張量中,其形狀爲
(samples, frames, height, width, color_depth)
舉個例子,一個以每秒 4 幀採樣的 60 YouTube 視頻片段,視頻尺寸爲 144×256,這個
視頻共有 240 幀。4 個這樣的視頻片段組成的批量將保存在形狀爲 (4, 240, 144, 256, 3)
的張量中。總共有 106 168 320 個值!如果張量的數據類型(dtype)是 float32,每個值都是  #106168320*4/2^20=405
32 位,那麼這個張量共有 405MB。好大!你在現實生活中遇到的視頻要小得多,因爲它們不以
float32 格式存儲,而且通常被大大壓縮,比如 MPEG 格式。
2.3 神經網絡的“齒輪”:張量運算
所有計算機程序最終都可以簡化爲二進制輸入上的一些二進制運算(ANDORNOR 等),
與此類似,深度神經網絡學到的所有變換也都可以簡化爲數值數據張量上的一些張量運算(tensor
operation),例如加上張量、乘以張量等。
在最開始的例子中,我們通過疊加 Dense 層來構建網絡。Keras 層的實例如下所示。

dense

英 /dens/

美 /dens/

adj. 稠密的;濃厚的;愚鈍的

keras.layers.Dense(512, activation='relu')

 

 

這個層可以理解爲一個函數,輸入一個 2D 張量,返回另一個 2D 張量,即輸入張量的新  #我認爲此處應該是輸出一個新表示
表示。具體而言,這個函數如下所示(其中 W 是一個 2D 張量,b 是一個向量,二者都是該層的
屬性)。
output = relu(dot(W, input) + b)
我們將上式拆開來看。這裏有三個張量運算:輸入張量和張量 W 之間的點積運算(dot)、
得到的 2D 張量與向量 b 之間的加法運算(+)、最後的 relu 運算。relu(x) max(x, 0)
注意 雖然本節的內容都是關於線性代數表達式,但你卻找不到任何數學符號。我發現,對於
沒有數學背景的程序員來說,如果用簡短的 Python 代碼而不是數學方程來表達數學概念,
他們將更容易掌握。所以我們自始至終將會使用 Numpy 代碼。
2.3.1 逐元素運算
relu 運算和加法都是逐元素element-wise)的運算,即該運算獨立地應用於張量中的每
個元素,也就是說,這些運算非常適合大規模並行實現(向量化實現,這一術語來自於 1970
1990 年間向量處理器超級計算機架構)。如果你想對逐元素運算編寫簡單的 Python 實現,那麼
可以用 for 循環。下列代碼是對逐元素 relu 運算的簡單實現。
 
根據同樣的方法,你可以實現逐元素的乘法、減法等。
在實踐中處理 Numpy 數組時,這些運算都是優化好的 Numpy 內置函數,這些函數將大量
運算交給安裝好的基礎線性代數子程序(BLAS,basic linear algebra subprograms)實現(沒裝
的話,應該裝一個)。BLAS 是低層次的、高度並行的、高效的張量操作程序,通常用 Fortran
或 C 語言來實現。
因此,在 Numpy 中可以直接進行下列逐元素運算,速度非常快。
2.3.2 廣播
上一節 naive_add 的簡單實現僅支持兩個形狀相同的 2D 張量相加。但在前面介紹的
Dense 層中,我們將一個 2D 張量與一個向量相加。如果將兩個形狀不同的張量相加,會發生
什麼?
如果沒有歧義的話,較小的張量會被廣播broadcast),以匹配較大張量的形狀。廣播包含
以下兩步。
(1) 向較小的張量添加軸(叫作廣播軸),使其 ndim 與較大的張量相同。
(2) 將較小的張量沿着新軸重複,使其形狀與較大的張量相同。
來看一個具體的例子。假設 X 的形狀是 (32, 10)y 的形狀是 (10,)。首先,我們給 y
添加空的第一個軸,這樣 y 的形狀變爲 (1, 10)。然後,我們將 y 沿着新軸重複 32 次,這樣
得到的張量 Y 的形狀爲 (32, 10),並且 Y[i, :] == y for i in range(0, 32)。現在,
我們可以將 X Y 相加,因爲它們的形狀相同。
在實際的實現過程中並不會創建新的 2D 張量,因爲那樣做非常低效。重複的操作完全是
虛擬的,它只出現在算法中,而沒有發生在內存中。但想象將向量沿着新軸重複 10 次,是一種
很有用的思維模型。下面是一種簡單的實現。
如果一個張量的形狀是 (a, b, ... n, n+1, ... m),另一個張量的形狀是 (n, n+1,
... m),那麼你通常可以利用廣播對它們做兩個張量之間的逐元素運算。廣播操作會自動應用
於從 a n-1 的軸。
下面這個例子利用廣播將逐元素的 maximum 運算應用於兩個形狀不同的張量。
 
2.3.3 張量點積
點積運算,也叫張量積tensor product,不要與逐元素的乘積弄混),是最常見也最有用的
張量運算。與逐元素的運算不同,它將輸入張量的元素合併在一起。
NumpyKerasTheano TensorFlow 中,都是用 * 實現逐元素乘積TensorFlow 中的
點積使用了不同的語法,但在 Numpy Keras 中,都是用標準的 dot 運算符來實現點積。
import numpy as np
z = np.dot(x, y)
數學符號中的點(.)表示點積運算。
z=x.y
從數學的角度來看,點積運算做了什麼?我們首先看一下兩個向量 x y 的點積。其計算
過程如下。
注意,兩個向量之間的點積是一個標量,而且只有元素個數相同的向量之間才能做點積。
你還可以對一個矩陣 x 和一個向量 y 做點積,返回值是一個向量,其中每個元素是 y x
的每一行之間的點積。其實現過程如下。
你還可以複用前面寫過的代碼,從中可以看出矩陣 - 向量點積與向量點積之間的關係。
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
注意,如果兩個張量中有一個的 ndim 大於 1,那麼 dot 運算就不再是對稱的,也就是說,
dot(x, y) 不等於 dot(y, x) #線性代數常識
當然,點積可以推廣到具有任意個軸的張量。最常見的應用可能就是兩個矩陣之間的點積。
對於兩個矩陣 x y,當且僅當 x.shape[1] == y.shape[0] 時,你纔可以對它們做點積 #前列等於後行
dot(x, y))。得到的結果是一個形狀爲 (x.shape[0], y.shape[1]) 的矩陣,其元素爲
的行與 y 的列之間的點積。其簡單實現如下。
 

而對於矩陣來說:

        shape[0]:表示矩陣的行數

        shape[1]:表示矩陣的列數

對於圖像來說:

        img.shape[0]:圖像的垂直尺寸(高度)

        img.shape[1]:圖像的水平尺寸(寬度)

        img.shape[2]:圖像的通道數

 
爲了便於理解點積的形狀匹配,可以將輸入張量和輸出張量像圖 2-5 中那樣排列,利用可
視化來幫助理解。
 
2-5 中,xy z 都用矩形表示(元素按矩形排列)。x 的行和 y 的列必須大小相同,因
x 的寬度一定等於 y 的高度。如果你打算開發新的機器學習算法,可能經常要畫這種圖。
更一般地說,你可以對更高維的張量做點積,只要其形狀匹配遵循與前面 2D 張量相同的
原則:
(a, b, c, d) . (d,) -> (a, b, c)
(a, b, c, d) . (d, e) -> (a, b, c, e)
以此類推。
2.3.4 張量變形
第三個重要的張量運算是張量變形tensor reshaping)。雖然前面神經網絡第一個例子的
Dense 層中沒有用到它,但在將圖像數據輸入神經網絡之前,我們在預處理時用到了這個運算。
train_images = train_images.reshape((60000, 28 * 28))
張量變形是指改變張量的行和列,以得到想要的形狀。變形後的張量的元素總個數與初始
張量相同。簡單的例子可以幫助我們理解張量變形。
 
下面這些矩陣裏面的.我認爲是一種小數表示方法
比如我測試發現:
>>> b = np.array([(1.5,2,3), (4,5,6)])
>>> b
array([[ 1.5,  2. ,  3. ],
       [ 4. ,  5. ,  6. ]])
>>> x = np.array([[0., 1.],
[2., 3.],
[4., 5.]])
>>> print(x.shape)
(3, 2)
>>> x = x.reshape((6, 1))
>>> x
array([[ 0.],
[ 1.],
[ 2.],
[ 3.],
[ 4.],
[ 5.]])
>>> x = x.reshape((2, 3))
>>> x
array([[ 0., 1., 2.],
[ 3., 4., 5.]])
經常遇到的一種特殊的張量變形是轉置transposition)。對矩陣做轉置是指將行和列互換,
使 x[i, :] 變爲 x[:, i]
2.3.5 張量運算的幾何解釋
對於張量運算所操作的張量,其元素可以被解釋爲某種幾何空間內點的座標,因此所有的
張量運算都有幾何解釋。舉個例子,我們來看加法。首先有這樣一個向量:
A = [0.5, 1]
它是二維空間中的一個點(見圖 2-6)。常見的做法是將向量描繪成原點到這個點的箭頭,
如圖 2-7 所示。
假設又有一個點:B = [1, 0.25],將它與前面的 A 相加。從幾何上來看,這相當於將兩
個向量箭頭連在一起,得到的位置表示兩個向量之和對應的向量(見圖 2-8
通常來說,仿射變換、旋轉、縮放等基本的幾何操作都可以表示爲張量運算。舉個例子,要將
一個二維向量旋轉 theta 角,可以通過與一個 2×2 矩陣做點積來實現,這個矩陣爲 R = [u, v],其
u v 都是平面向量:u = [cos(theta), sin(theta)]v = [-sin(theta), cos(theta)]
2.3.6 深度學習的幾何解釋
前面講過,神經網絡完全由一系列張量運算組成,而這些張量運算都只是輸入數據的幾何
變換。因此,你可以將神經網絡解釋爲高維空間中非常複雜的幾何變換,這種變換可以通過許
多簡單的步驟來實現。
對於三維的情況,下面這個思維圖像是很有用的。想象有兩張彩紙:一張紅色,一張藍色。
將其中一張紙放在另一張上。現在將兩張紙一起揉成小球。這個皺巴巴的紙球就是你的輸入數
據,每張紙對應於分類問題中的一個類別。神經網絡(或者任何機器學習模型)要做的就是找
到可以讓紙球恢復平整的變換,從而能夠再次讓兩個類別明確可分。通過深度學習,這一過程
可以用三維空間中一系列簡單的變換來實現,比如你用手指對紙球做的變換,每次做一個動作,
如圖 2-9 所示。
讓紙球恢復平整就是機器學習的內容:爲複雜的、高度摺疊的數據流形找到簡潔的表示。
現在你應該能夠很好地理解,爲什麼深度學習特別擅長這一點:它將複雜的幾何變換逐步分解
爲一長串基本的幾何變換,這與人類展開紙球所採取的策略大致相同。深度網絡的每一層都通
過變換使數據解開一點點——許多層堆疊在一起,可以實現非常複雜的解開過程。
2.4 神經網絡的“引擎”:基於梯度的優化
上一節介紹過,我們的第一個神經網絡示例中,每個神經層都用下述方法對輸入數據進行
變換。
output = relu(dot(W, input) + b)
在這個表達式中,W 和 b 都是張量,均爲該層的屬性。它們被稱爲該層的權重(weight)或
可訓練參數(trainable parameter),分別對應 kernel 和 bias 屬性。這些權重包含網絡從觀察
訓練數據中學到的信息。
一開始,這些權重矩陣取較小的隨機值,這一步叫作隨機初始化random initialization)。
當然,W b 都是隨機的,relu(dot(W, input) + b) 肯定不會得到任何有用的表示。雖然
得到的表示是沒有意義的,但這是一個起點。下一步則是根據反饋信號逐漸調節這些權重。這
個逐漸調節的過程叫作訓練,也就是機器學習中的學習。
上述過程發生在一個訓練循環training loop)內,其具體過程如下。必要時一直重複這些
步驟。
(1) 抽取訓練樣本 x 和對應目標 y 組成的數據批量。
(2) 在 x 上運行網絡[這一步叫作前向傳播(forward pass)],得到預測值 y_pred。
(3) 計算網絡在這批數據上的損失,用於衡量 y_pred 和 y 之間的距離。
(4) 更新網絡的所有權重,使網絡在這批數據上的損失略微下降。
最終得到的網絡在訓練數據上的損失非常小,即預測值 y_pred 和預期目標 y 之間的距離
非常小。網絡“學會”將輸入映射到正確的目標。乍一看可能像魔法一樣,但如果你將其簡化
爲基本步驟,那麼會變得非常簡單。
第一步看起來非常簡單,只是輸入 / 輸出(I/O)的代碼。第二步和第三步僅僅是一些張量
運算的應用,所以你完全可以利用上一節學到的知識來實現這兩步。難點在於第四步:更新網
絡的權重。考慮網絡中某個權重係數,你怎麼知道這個係數應該增大還是減小,以及變化多少?
一種簡單的解決方案是,保持網絡中其他權重不變,只考慮某個標量係數,讓其嘗試不同
的取值。假設這個係數的初始值爲 0.3。對一批數據做完前向傳播後,網絡在這批數據上的損失
0.5。如果你將這個係數的值改爲 0.35 並重新運行前向傳播,損失會增大到 0.6。但如果你將
這個係數減小到 0.25,損失會減小到 0.4。在這個例子中,將這個係數減小 0.05 似乎有助於使
損失最小化。對於網絡中的所有係數都要重複這一過程。
但這種方法是非常低效的,因爲對每個係數(係數很多,通常有上千個,有時甚至多達上
百萬個)都需要計算兩次前向傳播(計算代價很大)。一種更好的方法是利用網絡中所有運算都
是可微(differentiable)的這一事實,計算損失相對於網絡係數的梯度(gradient),然後向梯度
的反方向改變係數,從而使損失降低。
如果你已經瞭解可微梯度這兩個概念,可以直接跳到 2.4.3 節。如果不瞭解,下面兩小節
有助於你理解這些概念。
2.4.1 什麼是導數
假設有一個連續的光滑函數 f(x) = y,將實數 x 映射爲另一個實數 y。由於函數是連續的
x 的微小變化只能導致 y 的微小變化——這就是函數連續性的直觀解釋。假設 x 增大了一個很
小的因子 epsilon_x,這導致 y 也發生了很小的變化,即 epsilon_y
f(x + epsilon_x) = y + epsilon_y
此外,由於函數是光滑的(即函數曲線沒有突變的角度),在某個點 p 附近,如果 epsilon_x
足夠小,就可以將 f 近似爲斜率爲 a 的線性函數,這樣 epsilon_y 就變成了 a * epsilon_x
f(x + epsilon_x) = y + a * epsilon_x
顯然,只有在 x 足夠接近 p 時,這個線性近似纔有效。
斜率 a 被稱爲 f p 點的導數derivative)。如果 a 是負的,說明 x p 點附近的微小變
化將導致 f(x) 減小(如圖 2-10 所示);如果 a 是正的,那麼 x 的微小變化將導致 f(x) 增大。
此外,a 的絕對值(導數大小)表示增大或減小的速度快慢。
對於每個可微函數 f(x)可微的意思是“可以被求導”。例如,光滑的連續函數可以被求導),
都存在一個導數函數 f'(x),將 x 的值映射爲 f 在該點的局部線性近似的斜率。例如,cos(x)
的導數是 -sin(x)f(x) = a * x 的導數是 f'(x) = a,等等。
如果你想要將 x 改變一個小因子 epsilon_x,目的是將 f(x) 最小化,並且知道 f 的導數,
那麼問題解決了:導數完全描述了改變 x f(x) 會如何變化。如果你希望減小 f(x) 的值,只
需將 x 沿着導數的反方向移動一小步。
2.4.2 張量運算的導數:梯度
梯度gradient)是張量運算的導數。它是導數這一概念向多元函數導數的推廣。多元函數
是以張量作爲輸入的函數。
假設有一個輸入向量 x、一個矩陣 W、一個目標 y 和一個損失函數 loss。你可以用 W 來計
算預測值 y_pred,然後計算損失,或者說預測值 y_pred 和目標 y 之間的距離。
y_pred = dot(W, x)
loss_value = loss(y_pred, y)
如果輸入數據 x y 保持不變,那麼這可以看作將 W 映射到損失值的函數。
loss_value = f(W)
假設 W 的當前值爲 W0f W0 點的導數是一個張量 gradient(f)(W0),其形狀與 W 相同,
每個係數 gradient(f)(W0)[i, j] 表示改變 W0[i, j] loss_value 變化的方向和大小。
張量 gradient(f)(W0) 是函數 f(W) = loss_value W0 的導數。
前面已經看到,單變量函數 f(x) 的導數可以看作函數 f 曲線的斜率。同樣,gradient(f)
(W0) 也可以看作表示 f(W) W0 附近曲率curvature)的張量。
對於一個函數 f(x),你可以通過將 x 嚮導數的反方向移動一小步來減小 f(x) 的值。同
樣,對於張量的函數 f(W),你也可以通過將 W 向梯度的反方向移動來減小 f(W),比如 W1 =
W0 - step * gradient(f)(W0),其中 step 是一個很小的比例因子。也就是說,沿着曲
率的反方向移動,直觀上來看在曲線上的位置會更低。注意,比例因子 step 是必需的,因爲
gradient(f)(W0) 只是 W0 附近曲率的近似值,不能離 W0 太遠。
2.4.3 隨機梯度下降
給定一個可微函數,理論上可以用解析法找到它的最小值:函數的最小值是導數爲 0 的點,
因此你只需找到所有導數爲 0 的點,然後計算函數在其中哪個點具有最小值。
將這一方法應用於神經網絡,就是用解析法求出最小損失函數對應的所有權重值。可以通
過對方程 gradient(f)(W) = 0 求解 W 來實現這一方法。這是包含 N 個變量的多項式方程,
其中 N 是網絡中係數的個數。N=2 N=3 時可以對這樣的方程求解,但對於實際的神經網絡是
無法求解的,因爲參數的個數不會少於幾千個,而且經常有上千萬個。
相反,你可以使用 2.4 節開頭總結的四步算法:基於當前在隨機數據批量上的損失,一點
一點地對參數進行調節。由於處理的是一個可微函數,你可以計算出它的梯度,從而有效地實
現第四步。沿着梯度的反方向更新權重,損失每次都會變小一點。
(1) 抽取訓練樣本 x 和對應目標 y 組成的數據批量。
(2) x 上運行網絡,得到預測值 y_pred
(3) 計算網絡在這批數據上的損失,用於衡量 y_pred y 之間的距離。
(4) 計算損失相對於網絡參數的梯度[一次反向傳播backward pass)]。
(5) 將參數沿着梯度的反方向移動一點,比如 W -= step * gradient,從而使這批數據
上的損失減小一點。
這很簡單!我剛剛描述的方法叫作小批量隨機梯度下降mini-batch stochastic gradient descent
又稱爲小批量 SGD)。術語隨機stochastic)是指每批數據都是隨機抽取的(stochastic random
在科學上的同義詞 a)。圖 2-11 給出了一維的情況,網絡只有一個參數,並且只有一個訓練樣本。
如你所見,直觀上來看,爲 step 因子選取合適的值是很重要的。如果取值太小,則沿着
曲線的下降需要很多次迭代,而且可能會陷入局部極小點。如果取值太大,則更新權重值之後
可能會出現在曲線上完全隨機的位置。
注意,小批量 SGD 算法的一個變體是每次迭代時只抽取一個樣本和目標,而不是抽取一批
數據。這叫作SGD(有別於小批量 SGD)。還有另一種極端,每一次迭代都在所有數據上
運行,這叫作批量 SGD。這樣做的話,每次更新都更加準確,但計算代價也高得多。這兩個極
端之間的有效折中則是選擇合理的批量大小。
2-11 描述的是一維參數空間中的梯度下降,但在實踐中需要在高維空間中使用梯度下降。
神經網絡的每一個權重參數都是空間中的一個自由維度,網絡中可能包含數萬個甚至上百萬個
參數維度。爲了讓你對損失曲面有更直觀的認識,你還可以將梯度下降沿着二維損失曲面可視化,
如圖 2-12 所示。但你不可能將神經網絡的實際訓練過程可視化,因爲你無法用人類可以理解的
方式來可視化 1 000 000 維空間。因此最好記住,在這些低維表示中形成的直覺在實踐中不一定
總是準確的。這在歷史上一直是深度學習研究的問題來源。
此外,SGD 還有多種變體,其區別在於計算下一次權重更新時還要考慮上一次權重更新,
而不是僅僅考慮當前梯度值,比如帶動量的 SGDAdagradRMSProp 等變體。這些變體被稱
優化方法optimization method)或優化器optimizer)。其中動量的概念尤其值得關注,它在
許多變體中都有應用。動量解決了 SGD 的兩個問題:收斂速度和局部極小點。圖 2-13 給出了
損失作爲網絡參數的函數的曲線。
如你所見,在某個參數值附近,有一個局部極小點local minimum):在這個點附近,向
左移動和向右移動都會導致損失值增大。如果使用小學習率的 SGD 進行優化,那麼優化過程可
能會陷入局部極小點,導致無法找到全局最小點。
使用動量方法可以避免這樣的問題,這一方法的靈感來源於物理學。有一種有用的思維圖像,
就是將優化過程想象成一個小球從損失函數曲線上滾下來。如果小球的動量足夠大,那麼它不會
卡在峽谷裏,最終會到達全局最小點。動量方法的實現過程是每一步都移動小球,不僅要考慮當
前的斜率值(當前的加速度),還要考慮當前的速度(來自於之前的加速度)。這在實踐中的是指,
更新參數 w 不僅要考慮當前的梯度值,還要考慮上一次的參數更新,其簡單實現如下所示。
 
2.4.4 鏈式求導:反向傳播算法
在前面的算法中,我們假設函數是可微的,因此可以明確計算其導數。在實踐中,神經網
絡函數包含許多連接在一起的張量運算,每個運算都有簡單的、已知的導數。例如,下面這個
網絡 f 包含 3 個張量運算 ab c,還有 3 個權重矩陣 W1W2 W3
f(W1, W2, W3) = a(W1, b(W2, c(W3)))
根據微積分的知識,這種函數鏈可以利用下面這個恆等式進行求導,它稱爲鏈式法則chain
rule):(f(g(x)))' = f'(g(x)) * g'(x)。將鏈式法則應用於神經網絡梯度值的計算,得
到的算法叫作反向傳播backpropagation,有時也叫反式微分reverse-mode differentiation)。反
向傳播從最終損失值開始,從最頂層反向作用至最底層,利用鏈式法則計算每個參數對損失值
的貢獻大小。
現在以及未來數年,人們將使用能夠進行符號微分symbolic differentiation)的現代框架來
實現神經網絡,比如 TensorFlow。也就是說,給定一個運算鏈,並且已知每個運算的導數,這
些框架就可以利用鏈式法則來計算這個運算鏈的梯度函數,將網絡參數值映射爲梯度值。對於
這樣的函數,反向傳播就簡化爲調用這個梯度函數。由於符號微分的出現,你無須手動實現反
向傳播算法。因此,我們不會在本節浪費你的時間和精力來推導反向傳播的具體公式。你只需
充分理解基於梯度的優化方法的工作原理。
2.5 回顧第一個例子
你已經讀到了本章最後一節,現在應該對神經網絡背後的原理有了大致的瞭解。我們回頭
看一下第一個例子,並根據前面三節學到的內容來重新閱讀這個例子中的每一段代碼。
下面是輸入數據。
(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
現在你明白了,輸入圖像保存在 float32 格式的 Numpy 張量中,形狀分別爲 (60000,
784)(訓練數據)和 (10000, 784)(測試數據)。
下面是構建網絡。
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
現在你明白了,這個網絡包含兩個 Dense 層,每層都對輸入數據進行一些簡單的張量運算,
這些運算都包含權重張量。權重張量是該層的屬性,裏面保存了網絡所學到的知識knowledge)。
下面是網絡的編譯。
network.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
現在你明白了,categorical_crossentropy 是損失函數,是用於學習權重張量的反饋
信號,在訓練階段應使它最小化。你還知道,減小損失是通過小批量隨機梯度下降來實現的。
梯度下降的具體方法由第一個參數給定,即 rmsprop 優化器。
最後,下面是訓練循環。
network.fit(train_images, train_labels, epochs=5, batch_size=128)
現在你明白在調用 fit 時發生了什麼:網絡開始在訓練數據上進行迭代(每個小批量包含
128 個樣本),共迭代 5 次[在所有訓練數據上迭代一次叫作一個輪次epoch)]。在每次迭代
過程中,網絡會計算批量損失相對於權重的梯度,並相應地更新權重。5 輪之後,網絡進行了
2345 次梯度更新(每輪 469 次),網絡損失值將變得足夠小,使得網絡能夠以很高的精度對手
寫數字進行分類。
到目前爲止,你已經瞭解了神經網絡的大部分知識。
本章小結
學習是指找到一組模型參數,使得在給定的訓練數據樣本和對應目標值上的損失函數最
小化。
學習的過程:隨機選取包含數據樣本及其目標值的批量,並計算批量損失相對於網絡參
數的梯度。隨後將網絡參數沿着梯度的反方向稍稍移動(移動距離由學習率指定)。
整個學習過程之所以能夠實現,是因爲神經網絡是一系列可微分的張量運算,因此可以
利用求導的鏈式法則來得到梯度函數,這個函數將當前參數和當前數據批量映射爲一個
梯度值。
後續幾章你會經常遇到兩個關鍵的概念:損失優化器。將數據輸入網絡之前,你需要
先定義這二者。
損失是在訓練過程中需要最小化的量,因此,它應該能夠衡量當前任務是否已成功解決。
優化器是使用損失梯度更新參數的具體方式,比如 RMSProp 優化器、帶動量的隨機梯
度下降(SGD)等。
 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章