人工神經網絡(Artificial Neural Network,ANN) 是一種受人腦生物神經網絡信息處理方式啟發而誕生的一種計算模型,得益於語音識別、計算機視覺和文本處理方面的許多突破性成果,人工神經網絡在機器學習研究和工業領域引起了強烈反響。本篇博客將對人工神經網絡算法進行介紹。
1 神經元節點¶
神經元是神經網絡的最基本單元,它接收一個外部輸入數據或其他神經元節點的輸出作為輸入,經過計算后將產生一個輸出。每個神經元節點都有一個權重和偏置參數,權重象征着對應的輸入相對於其他輸入的重要程度,而偏置則是用於對權重與輸入的計算結果進行校正,如圖1所示:
圖1中的神經元節點接受$x_i$作為輸入,然后與相關聯的權重參數$w_i$相乘后求和,加上偏置$b$后使用函數$f$進行變換輸出獲得$y$作為這一神經元節點的輸出值。對於函數$f$,通常我們稱之為激活函數,其作用是添加一個非線性變換,這是至關重要的一步操作,因為現實世界中的絕大多數數據都不會是呈線性分布,如果沒有為網絡中的神經元節點添加激活函數,整個網絡只不過是多個線性分類器的組合,依然只能完成線任務。所以,神經元節點中的數學運算我們可以更一般化得表達為: $$y = Activation(\sum\limits_i^N {{w_i} \cdot {x_i} + b} )$$
激活函數接受一個數值作為輸入,對該數值進行非線性運算之后輸出一個數值作為結果。下面說說常見的幾個激活函數。
(1)sigmod函數 $$f(x) = \frac{1}{{1 + {e^{ - x}}}}$$ sigmoid函數可以將整個實數范圍的的任意值映射到[0,1]范圍內,當輸入值較大時,sigmoid將返回一個接近於1的值,而當輸入值較小時,返回值將接近於0。sigmod函數圖像如圖2所示。
(2)Relu函數 $$f(x) = \max (0,x)$$ relu(Rectified Linear Units修正線性單元),是目前被使用最為頻繁得激活函數,relu函數在x<0時,輸出始終為0。由於x>0時,relu函數的導數為1,即保持輸出為x,所以relu函數能夠在x>0時保持梯度不斷衰減,從而緩解梯度消失的問題,還能加快收斂速度,還能是神經網絡具有稀疏性表達能力,這也是relu激活函數能夠被使用在深層神經網絡中的原因。由於當x<0時,relu函數的導數為0,導致對應的權重無法更新,這樣的神經元被稱為"神經元死亡"。relu函數圖像如圖3所示。
(3)softmax函數 $$f({x_i}) = \frac{{{e^{{x_i}}}}}{{\sum\limits_i {{e^{{x_i}}}} }}$$ softmax函數是sigmoid函數的進化,在處理分類問題是很方便,它可以將所有輸出映射到成概率的形式,即值在[0,1]范圍且總和為1。例如輸出變量為[1.5,4.4,2.0],經過softmax函數激活后,輸出為[0.04802413, 0.87279755, 0.0791784 ],分別對應屬於1、2、3類的概率。
2 前饋神經網絡¶
前饋神經網絡是的最原始也是最簡單的一種人工神經網絡。前饋神經網絡由多個層組成,每一層包含多個神經元節點,相鄰層直接神經元節點相互連接,每一條連接都有一個權重參數$w$與之關聯,如圖4所示。
前饋神經網絡中層節點可以分為3類。
(1)輸入層節點。輸入層節點是前饋神經網絡的第一層節點,作為外部數據到神經網絡的入口而存在,輸入層節點不負責進行任何技術,只是將外部輸入的數據傳遞到隱藏層節點。
(2)隱藏層節點。隱藏層節點是前饋神經網絡的中間部分,它不與外界進行直接接觸(這也是為什么成為隱藏層的原因)。隱藏層節點負責對輸出層節點傳遞過來的數據進行一定運算,然后傳遞給輸出層。前饋神經網絡可以零個或多個隱藏層。
(3)輸出層節點。輸出層節點是整個前饋神經網絡結構上最末尾上一層的節點,負責將隱藏層中傳遞過來的信息進行整合並輸出到外界。
在前饋網絡中,信息僅在一個方向上向前移動,即從輸入節點到隱藏節點(如果有)再到輸出節點不存在循環或回路的情況。我們可以粗略地將前饋神經網絡分為兩類:
(1)單層感知機模型。單層感知機模型是最簡單的一種神經網絡模型,不包含隱藏層,也只能處理線性任務。關於單層感知機模型,請參考我的另一篇博客。
(2)多層感知機。多層干自己是單程感知機的進化版,其組成結構中至少包含一個隱藏層,且在層與層之間添加了激活函數進行非線性變換,使其可以適用於非線性任務中。下文中,我們將圍繞多層感知機對神經網絡展開介紹。
3 多層感知機¶
如圖5所示是一個包含一個隱藏層的多層感知機,從圖中可以看出,節點直接的連接都有一個權值,當然,圖中只顯示了紅色節點的三個參數。
(1)輸入層。輸入層具有三個節點,偏置節點的值為$1$(有時候偏置並不會作為一個節點,而是作為下一層節點中的一個參數而存在),其它兩個節點將$X1$和$X2$作為外部輸入(其數值取決於輸入數據集)。如上所述,在輸入層中不執行任何計算,因此輸入層中節點的輸出分別為$1$、$X1$和$X2$,它們被饋送到隱藏層。
(2)隱藏層。圖5所示網絡結構隱藏層中同樣具有三個節點,其偏置節點的輸出為1,隱藏層中其他兩個節點的輸出取決於輸入層$(1,X1,X2)$的輸出以及與之相關的權值,權值與$(1,X1,X2)$運算后使用激活函數$f$進行非線性變換,最終的結果作為輸出傳遞到下一層。
(3)輸出層。輸出層具有兩個節點,這兩個節點從隱藏層獲取輸入,並執行與紅色隱藏層節點所示的類似計算,計算結果(Y1和Y2)用作為多層感知器的輸出。
給定一組特征$X =(x1,x2,…,xn)$和目標值$Y$,多層感知器可以學習特征與目標值之間的關系,以進行分類或回歸。舉例來說,假設有如下一個描述學生成績的數據集。兩個輸入列顯示了學生學習的小時數和學生獲得的期中成績,最終結果列可以有兩個值1或0,表示學生是否通過了最后一個學期。我們可以看到,如果學生學習了35個小時,並且在期中獲得了67分,那么他/她最終將通過期末考試。
現在,假設我們要預測一個學習25小時並在期中獲得70分的學生能否通過期末考試。這是一個二進制分類問題,多層感知器可以從給定的示例中學習(訓練數據),並在給定新數據點的情況下做出明智的預測。接下來,我們繼續說說多層感知機模型是如何進行學習的。
4 反向傳播算法¶
所謂神經網絡的學習或者說訓練,就是指通過不斷地調整參數優化解決指定問題所需的參數,使得神經網絡的輸出值與真實取值之間的差異最小化,這里的參數包括各層神經元之間的連接權重以及偏置等。這個學習的過程一般通過誤差反向傳播算法來完成。
誤差反向傳播算法是怎么工作的呢?
隨機初始化網絡中權重參數后,就可以開始進入訓練階段,依次將數據集中的數據輸入到網絡中,對應的,網絡模型會對每一個輸出進行相應產生一個輸出。將該輸出與我們已經知實際值進行比較,並將將誤差“傳播”回上一層,並相應地調整權重。重復該過程,直到輸出誤差低於預定閾值為止——這就是誤差反向傳播算法的大致過程。我們繼續用上文中學生學習時長、期中考成績與期末是否通過的例子說說誤差具體是怎么反向傳播的。
第一步:前向傳播
如圖5所示,假設從輸入到該節點的連接權重為$w1$,$w2$和$w3$。先將上文例子中第一條數據[35, 67, 1]輸入到網絡中,數據[35, 67, 1]可以拆分為兩個部分:
(1)特征向量[35, 67]
(2)目標向量[1, 0]
用$f$表示激活函數,$V$表示網絡輸出,那么,圖5的過程可以表示為:
$$V = f(1 \times w1 + 35 \times w2 + 67 \times w3)$$ 隱藏層中,另外兩個節點的輸出計算也是類似的,只不過在圖中並沒有表示出來。當三個隱藏層節點完成計算后,獲得的結果將繼續往下一層也就是輸出層傳輸,輸出層將輸出兩個概率值。假設輸出層中兩個節點的輸出概率分別為0.4和0.6(由於權重是隨機分配的,因此輸出也將是隨機的)。我們可以看到,計算出的概率(0.4和0.6)與期望的概率(分別為1和0)相距甚遠,誤差較大,不過到此第一次的前向傳播就完成了。
第二步:反向傳播與參數更新
我們計算輸出節點處的總誤差,將這些誤差傳播回網絡,然后我們使用梯度下降法法來更新網絡中的所有參數,以減少輸出層的誤差。如下圖6所示(現在請姑且先忽略圖中公式,公式將在下文中用到)。假設上圖節點v的權重$w1$,$w2$,$w3$更新后權重為$w4$,$w5$,$w6$,更新過程如下所示:
如果我們現在再次向網絡輸入相同的樣本數據,則網絡的表現將會比之前更好,因為現在已經調整了權重使得預測誤差更小。如圖7所示,與之前的[0.6,-0.4]相比,現在輸出節點上的誤差減少到[0.2,-0.2]。
上述過程就完成了一次反向傳播過程,在大量樣本情況下,通過反向傳播更新參數不斷更新,網絡模型效果將得到不斷提升,從而解決實際問題。接下來,我們用TensorFlow來搭建一個人工神經網絡模型,實現服飾圖片分類。
5 Tensorflow實現人工神經網絡¶
我們使用Fashion Mnist數據集來訓練、測試一個神經網絡模型。Fashion Mnist數據集由70,000張黑白圖片構成,每張圖片大小為 28x28,由十類服飾圖片構成。我們使用60,000張圖片作為訓練集,10,000張圖片作為測試集。這個數據集可以從 TensorFlow 中直接獲取,返回值為numpy數組。
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow import keras
from tensorflow.keras import datasets, layers, optimizers, Sequential ,metrics
加載數據集
(x, y), (x_test, y_test) = datasets.fashion_mnist.load_data()
來看看數據集具體是什么樣的:
index = 1
fig, axes = plt.subplots(4, 3, figsize=(8, 4), tight_layout=True)
for row in range(4):
for col in range(3):
axes[row, col].imshow(x[index])
axes[row, col].axis('off')
axes[row, col].set_title(y[index])
index += 1
plt.show()
圖片上方數字為圖像對應的類別標簽,其具體含義如下圖表格所示:
定義一個函數對圖中像素進行處理:
def preprocess(x, y):
x = tf.cast(x, dtype=tf.float32) / 255.
y = tf.cast(y, dtype=tf.int32)
return x, y
print(x.shape, y.shape)
print(x_test.shape, y_test.shape)
(60000, 28, 28) (60000,) (10000, 28, 28) (10000,)
batchs = 128 # 每個簇的大小
打亂訓練數據集:
db = tf.data.Dataset.from_tensor_slices((x, y))
db = db.map(preprocess).shuffle(10000).batch(batchs)
對測試集,雖然不需要打亂,但是也要進行與數據集同樣的預處理和分割出簇:
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.map(preprocess).batch(batchs)
現在,數據已經准備就緒,可以開始搭建模型了。因為圖像大小為28*28,展開就是784大小,所以輸入層大小一定是784,我們可以從下一層,也就是第一個隱藏層開始定義,我們設置為256個節點,接下來各層分別是128,64,32個節點,最后一層為輸出層,因為有10個類別,所以我們在輸出層定義10個節點。
model = Sequential([
layers.Dense(256, activation=tf.nn.relu), # [b, 784] --> [b, 256]
layers.Dense(128, activation=tf.nn.relu), # [b, 256] --> [b, 128]
layers.Dense(64, activation=tf.nn.relu), # [b, 128] --> [b, 64]
layers.Dense(32, activation=tf.nn.relu), # [b, 64] --> [b, 32]
layers.Dense(10) # [b, 32] --> [b, 10]
]
)
model.build(input_shape=[None,28*28])
model.summary()
optimizer = optimizers.Adam(lr=1e-3)#1e-3
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) multiple 200960 _________________________________________________________________ dense_1 (Dense) multiple 32896 _________________________________________________________________ dense_2 (Dense) multiple 8256 _________________________________________________________________ dense_3 (Dense) multiple 2080 _________________________________________________________________ dense_4 (Dense) multiple 330 ================================================================= Total params: 244,522 Trainable params: 244,522 Non-trainable params: 0 _________________________________________________________________
從上面的輸出可以看出每一層參數量,例如第一層有參數200960個,對於一個簡單的圖像分類任務來說,這個參數量是十分巨大的,這也是為什么會有我們下一篇博客中將要介紹的卷積神經網絡誕生的原因之一。
定義訓練過程,這個過程通過模型的fit方法配置可以輕松完成,不過,為了展示神經網絡的具體工作流程,下面我們重新擼一遍代碼:
def main():
for epoch in range(10): # 迭代訓練30次后停止
train_loss = 0
train_num = 0
for step, (x, y) in enumerate(db):
# db分成batch后,大概有469個batch
# x的shape(batchs,28,28),因為數據集已經分成了一個個簇,所以每一個x都是是一個簇,包含batchs張圖片
# 為方便運算,重新將x reshape成二維數據,x的shape就變成了(batchs, 784)
x = tf.reshape(x, [-1, 28*28])
with tf.GradientTape() as tape: # 梯度
logits = model(x) # 使用模型對x進行計算,輸出為預測值,也就是整個模型的輸出。輸出的logits的shape為(batchs, 10)
y_onehot = tf.one_hot(y,depth=10) # 對y進行熱獨編碼
loss_mse = tf.reduce_mean(tf.losses.MSE(y_onehot, logits))
loss_ce = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
loss_ce = tf.reduce_mean(loss_ce) # 計算整個簇的平均loss
grads = tape.gradient(loss_ce, model.trainable_variables) # 計算梯度
optimizer.apply_gradients(zip(grads, model.trainable_variables)) # 更新梯度
if step % 300 == 0: # 沒訓練完300個簇輸出一次loss
print((epoch), step, 'loss:', float(loss_ce), float(loss_mse))
train_loss += float(loss_ce)
train_num += x.shape[0]
loss = train_loss / train_num
total_correct = 0
total_num = 0
for x,y in db_test:# 每一次迭代都是用測試集驗證一下准確率
x = tf.reshape(x, [-1, 28*28]) # 跟上面訓練的時候一樣,x的shape也是(batchs,28,28)所以reshape成二維的shape為(128,784)
logits = model(x) # 進行預測,logits是(batchs, 10)的tensor
prob = tf.nn.softmax(logits, axis=1) # 將logits里面每一個(每一個預測值)轉換為概率的形式,保證概率和為1
pred = tf.argmax(prob, axis=1) #每一行最大概率所在索引
pred = tf.cast(pred, dtype=tf.int32)
correct = tf.equal(pred, y) # 判斷預測值是否與真實值相等
correct = tf.reduce_sum(tf.cast(correct, dtype=tf.int32)) # 統計相等的個數
total_correct += int(correct)
total_num += x.shape[0]
acc = total_correct / total_num
print(epoch, 'avg-loss:',loss,'test acc:', acc)
調用main()方法,開始訓練模型:
main()
0 0 loss: 0.08061596751213074 296.3019714355469 0 300 loss: 0.176801860332489 353.3863220214844 0 avg-loss: 0.0007949150484676162 test acc: 0.8895 1 0 loss: 0.11498871445655823 387.3428955078125 1 300 loss: 0.05147100239992142 411.7041015625 1 avg-loss: 0.0007653930709076425 test acc: 0.8895 2 0 loss: 0.10222060978412628 440.11517333984375 2 300 loss: 0.0964309498667717 398.7025451660156 2 avg-loss: 0.0007472941488958896 test acc: 0.8909 3 0 loss: 0.10470890998840332 319.424560546875 3 300 loss: 0.09943300485610962 411.2906494140625 3 avg-loss: 0.0007366230035511156 test acc: 0.8919 4 0 loss: 0.09135544300079346 442.5450744628906 4 300 loss: 0.11110477149486542 483.79437255859375 4 avg-loss: 0.0007339626496657729 test acc: 0.8945 5 0 loss: 0.059614263474941254 422.6461486816406 5 300 loss: 0.10703736543655396 485.8603210449219 5 avg-loss: 0.0007124265033130844 test acc: 0.8916 6 0 loss: 0.12480953335762024 364.9261169433594 6 300 loss: 0.08893868327140808 568.0016479492188 6 avg-loss: 0.0006790677736513317 test acc: 0.8908 7 0 loss: 0.1477620005607605 367.45355224609375 7 300 loss: 0.08248952776193619 461.7186279296875 7 avg-loss: 0.0006600742932719489 test acc: 0.8915 8 0 loss: 0.07141244411468506 549.00341796875 8 300 loss: 0.08829254657030106 616.8319702148438 8 avg-loss: 0.0006306016177870333 test acc: 0.8842 9 0 loss: 0.12584742903709412 347.49615478515625 9 300 loss: 0.08275362849235535 383.50506591796875 9 avg-loss: 0.0006549825438919167 test acc: 0.8911
可以看到,經過30次迭代訓練之后,模型的准確率基本穩定在左右89%。