人工神經網絡
通過前面的 6 節實驗,我們學習了基于傳統特征提取算法構建一個目標檢測項目,但是傳統的算法需要花費大量時間人為的設計特征,在實際應用中面對復雜的背景和目標時往往表現得并不理想。但是隨著深度學習的崛起目標檢測的性能和表現得到了大幅度的提升,深度學習的發展推動了目標檢測的迅猛發展。在接下來的幾節實驗我們將向大家逐步介紹基于深度學習的目標檢測算法。
深度學習是人工智能的一個分支,其受人腦的結構和功能的啟發,通過人工神經網絡(Artificial Neural Network)模仿人腦處理數據和決策的方式從數據中學習內在規律和特征表示。隨著近年的計算機性能的發展和海量數據的增長使得深度學習成為機器學習中的熱門研究方向。如今深度學習已經廣泛應用于我們的日常生活中,如在線翻譯、人臉識別、語音轉換等。
人工神經網絡(Artificial Neural Network)是一類可以從提供的數據中學習的機器學習算法,是一種模仿人腦神經系統處理信息的運算模型,其由大量的節點或稱為神經元相互連接構成。一個節點由輸入(Input)、權重(Weight)、偏差(Bias)、激活函數(Activation Function)、輸出(Output)組成。下圖左邊的圖片是一個簡單的三層神經網絡,其中每一個圓形表示一個節點且每一個節點都與下一層中的每個節點相連接。黃色節點所在的層被稱為輸入層(Input Layer),綠色節點所在的層被稱為輸出層(Output Layer),在神經網絡中除去輸入層和輸出層的部分被稱為隱藏層(Hidden Layer)。
上圖右邊的圖片是一個節點或稱為神經元,x1 ,x2 ,x3...xn表神經元的輸入(這些x可以是像素值、語音、文字等),每一個x都有一個與之對應的權重w。這些權重值在訓練過程中會被不斷地更新,擁有較高權重值的x會被認為是比較重要的信息, 反之擁有較低權重值的信息會被認為不太重要。b表示偏差。我們將x和w相乘再求和,將求和結果加上偏差后輸入一個激活函數得出最后的輸出。這就是一個感知器的典型結構。將計算過程用數學方式來表達就是下面的公式。
激活函數(Activation Function)
激活函數為人工神經網絡處理非線性問題提供了重要作用,其引入了非線性因素并且為節點建立一個輸出邊界,激活函數增加了神經網絡的復雜性和網絡學習復雜事物的能力。常見的激活函數有 Step、Sigmoid、Tanh、ReLU、Leaky ReLU、ELU,這里我們將向大家介紹 Sigmoid 函數。
Sigmoid 函數是神經網絡中一個常見的激活函數。下面是 sigmoid 公式,其中x是上面求和的結果y。
下圖是 Sigmoid 函數圖,它的值在 0 到 1 之間。可以看到x越小y越接近 0,反之x越大y越接近 1。Sigmoid 函數曾被大量使用,但是由于其會導致梯度反向傳遞時,梯度爆炸和梯度消失,近年來使用越來越少。
前饋神經網絡(Feed Forward Network)
前饋神經網絡或前饋網絡是深度學習中常見的一種單向網絡,每一層的神經元只接收前一層神經元的輸出并將自身的輸出傳遞給下一層的神經元。信息從輸入層逐層傳遞最后從輸出層輸出結果,整個網絡沒有反饋和中間跳轉層。
上圖是一個簡單的前饋網絡,為了方便描述一個前饋網絡,我們可以使用一組整數來表示網絡每層的節點數。例如上圖的網絡我們可以表示為 2-3-2,表示第一層有 2 個節點,第二層有 3 個節點,第三層有 2 個節點。我們使用整數 0 表示輸入層,1 表示隱藏層,2 表示輸出層。通常輸出層會有多個節點,例如我們使用神經網絡對人膚色分類時,輸出層會有三個節點分別表示黃色、白色、黑色。
反向傳播(Back Propagation)
在使用神經網絡解決問題時,信息經過神經網絡的前向傳播最后得出的結果會與期望的結果有偏差,這時我們需要計算實際輸出結果和期望結果之間的誤差,并將該誤差反向地從輸出層向輸入層傳播,以此來更新權重以達到優化神經網絡的目的,這個過程就是神經網絡訓練的過程。
使用 Python 構建一個神經網絡
下面我們構建一個神經網絡,首先導入 NumPy 模塊。然后我們設定一個學習率 alpha 用于控制網絡的學習進度,這里我們設定一個值為 0.1。
import numpy as np
alpha = 0.1
然后我們創建一個 set_w 函數對神經網絡進行一些初始化處理。該函數有一個輸入值 layers,這個輸入值是一個列表,表示網絡的結構,例如我們給函數輸入一個列表 [3, 2, 1] 表示這個網絡的輸入層有 3 個節點,隱藏層有兩個節點,輸出層有 1 個節點。
def set_w(layers):
W = [np.random.randn(x + 1, y + 1) / np.sqrt(x)
for x, y in zip(layers[:-1], layers[1:-1])]
w = np.random.randn(layers[-2] + 1, layers[-1])
W.append(w / np.sqrt(layers[-2]))
print("Network Layers: {}".format("-".join(str(n) for n in layers)))
return W
下面我們構建一個 sigmoid 激活函數,該函數需要一個輸入值 x。在函數內我們根據前面提到的激活函數公式計算激活值并返回計算結果。
def sigmoid(x):
return 1.0 / (1 + np.exp(-x))
接下來我們構建一個 sigmoid_deriv 函數,該函數同樣需要一個輸入值 x。在函數內我們計算 sigmoid 函數的導數并返回計算結果。
def sigmoid_deriv(x):
return x * (1 - x)
下面我們構建一個 feedforward 函數,該函數需要一個輸入值 data, 這個 data 表示輸入網絡的數據集。我們使用這個函數實現前向傳播,當訓練完神經網絡,我們將使用這個函數進行結果預測。在函數內首先使用 np.atleast_2d 確保 data 至少是 2 維數組。因為我們將偏置添加進了權重矩陣,所以在第三行代碼中我們使用 np.c_ 在數組的每一行的末尾添加一個 1。
代碼的第 5、6 行表示我們用 for 獲取 W 中每個權重矩陣,然后分別使用矩陣乘法和 sigmoid 函數對數據進行預測。最后返回預測值。
def feedforward(data):
p = np.atleast_2d(data)
p = np.c_[p, np.ones(p.shape[0])]
for layer in np.arange(0, len(W)):
p = sigmoid(np.dot(p, W[layer]))
return p
下面我們創建一個 loss 函數用于計算實際輸出結果和期望結果之間的誤差。該函數有兩個輸入值,data 數據集和數據集中每個數據對應的標簽。在函數內我們首先使用 np.atleast_2d 確保 data 至少是 2 維數組,然后使用 feedforward 函數計算數據集的預測結果 predictions。最后我們計算預測結果與真實標簽的誤差 loss。
def loss(data, y):
y = np.atleast_2d(y)
predictions = feedforward(data)
loss = 0.5 * np.sum((predictions - y) ** 2)
return loss
接下來我們將創建一個 backprop 函數, 這個函數將用于計算反向傳播。該函數需要兩個輸入值 x 和 y 分別表示數據集中的每個數據和其對應的標簽。下面的第 2 至 7 行代碼與 feedforward 函數類似這里就不多贅述了,第 8 行代碼先將網絡的每層矩陣相乘結果作為 sigmoid 函數的輸入,然后將函數計算結果添加到列表 A 中。
第 10 行代碼開始就是反向傳播的過程,首先計算網絡的輸出值與標簽值的差,這一步其實是loss 函數的導數。第 11 行代碼我們創建一個列表 D 用于存儲梯度變化的量,根據鏈式法則計算 error 與 sigmoid 函數的導數的乘積。列表中的值將用于更新權重矩陣。
def backprop(x, y):
a = np.atleast_2d(x)
A = [np.c_[a, np.ones((a.shape[0]))]]
for layer in np.arange(0, len(W)):
out = sigmoid(A[layer].dot(W[layer]))
A.append(out)
error = A[-1] - y
D = [error * sigmoid_deriv(A[-1])]
for layer in np.arange(len(A) - 2, 0, -1):
delta = D[-1].dot(W[layer].T)
delta = delta * sigmoid_deriv(A[layer])
D.append(delta)
D = D[::-1]
for layer in np.arange(0, len(W)):
W[layer] += -alpha * A[layer].T.dot(D[layer])
第 13 到 16 行計算每層的梯度變化量,我們使用 for 循環反向遍歷網絡的每一層(不包括最后一層,因為最后一層網絡的梯度變化我們已經在第 11 行計算了)。第 14、15 行計算當前層的梯度變化 delta, delta 就等于前一層的梯度變化 D[-1] 與當前層的權重的轉置矩陣相乘,然后再與當前層的 sigmoid 函數的導數相乘。第 16 行將計算后得到的 delta 添加到列表 D 中。
上面第 18 到 21 行將更新權重矩陣。第 18 行將顛倒 D 中 delta 的順序,因為 delta 是通過反向傳播從輸出層向輸入層計算的,所以在更新權重矩陣時要將其順序顛倒。第 20、21 行使用 for 循環遍歷網絡的每一層,在每一層我們將當前層的激活函數的轉置矩陣和 D 中的 delta 相乘,再乘以負的學習率 alpha,最后我將計算得到的值與當前層的權重相加即可完成權重的更新。
下面是創建一個 train 函數用于訓練網絡。該函數需要 3 個輸入值,data 表示輸入的數據集,y 是每個數據的標簽,epochs 表示訓練的次數,這里我們設置一個默認值 500。
def train(data, y, epochs = 500):
for epoch in np.arange(0, epochs):
for (x, label) in zip(data, y):
backprop(x, label)
print("epoch: {}, loss: {}".format(epoch + 1, loss(data, y)))
在函數內首先使用一個 for 循環用于執行 epochs 次訓練。接下來使用一個 for 循環獲取 data 和 y 中的每個數據 x 和其對應的標簽 label,在循環內我們執行 backprop 函數,將獲取的數據和標簽作為函數的輸入值。最后輸出每次訓練后的誤差。
至此我們的神經網絡已經構建完成了,下面我們使用一個手寫數字的數據集來訓練搭建的網絡并對數字進行分類識別。我們將從 Scikit-learn 中導入這個數據集(見下圖),該數據集包含 1797 張數字,每張數字圖片的尺寸是8×8,每一數字圖像在 Scikit-learn 中被處理為一維向量。
首先我們將導入需要用到的模塊
from sklearn import datasets
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
然后我們使用從 sklearn 導入的 dataset 模塊中的 load_digits 方法加載數據集,并且將數據集中的數據類型轉換為浮點型。
digits = datasets.load_digits()
data = digits.data.astype("float")
接下來使用從 sklearn 的 model_selection 模塊中導入 train_test_split 方法將數據集分為訓練集和測試集,這里我們將數據集中的 25% 數據分為測試集(關于 train_test_split 方法的詳細講解可以參考前面的線性分類實驗)。
(trainX, testX, trainY, testY) = train_test_split(data, digits.target, test_size=0.25)
然后我們使用從 sklearn 的 preprocessing 模塊中導入的 LabelBinarizer 對數據集的標簽進行編碼,具體操作是使用 LabelBinarizer 中的 fit_trainsform 方法分別將訓練集和測試集的標簽二值化。
trainY = LabelBinarizer().fit_transform(trainY)
testY = LabelBinarizer().fit_transform(testY)
下面我們可以使用神經網絡訓練模型了,首先我們調用 set_w 函數初始化我們的權重矩陣,trainX.shape[1] 表示獲取每個數字圖片的像素個數,前面我們提到每張數字圖片的尺寸是8×8,所以輸入層的節點數是 64,而由于數字從 0 到 9 共有 10 種,所以輸出層的節點個數是 10。
W = set_w([trainX.shape[1], 32,16,10])
訓練神經網絡的代碼很簡單,只需要調用 train 函數即可,這里我們將訓練集 trainX 和其對應的標簽 trainY 作為函數的輸入。
train(trainX, trainY)
為了評估模型的性能,在模型訓練完后我們需要對模型進行評估。首先我們調用 feedforward 函數對測試集進行預測,然后使用 argmax 方法找到概率最大的標簽值的下標。最后使用從 sklearn 的 metrics 模塊中導入的 classification_report 方法顯示評估的結果。
print("Evaluating model...")
predictions = feedforward(testX)
predictions = predictions.argmax(axis=1)
print(classification_report(testY.argmax(axis=1), predictions))
如果運行正常的,大家應該可以看到類似下圖的結果。可以看到從 0 到 9 每個數字預測的精準度以及整個測試集預測的精準率。