4.1 深度學習與深層神經網絡
假設一個模型的輸出 y 和輸入 x 滿足以下關系,那么這個模型為線性模型:
因為矩陣乘法滿足結合律,前向傳播算法可整理為:
說明前向傳播算法符合線性模型的定義,而線性模型無法解決非線性問題。
在 TensorFlow 游樂場中,激活函數選擇線性(Linear),訓練 107 輪后的結果如下圖所示,并不能很好解決分類的問題。
選擇另一個線性可分的數據集,線性模型在訓練 101 輪后很好的完成了分類問題。說明線性模型只能解決線性可分問題。
選擇線性不可分數據集和 ReLU 激活函數,訓練 100 輪之后,可以看到較好的完成了分類任務。說明了使用非線性模型解決線性不可分問題的效果更好。
加入激活函數和偏置項之后神經元的輸出變成了:
以下是幾個常用的非線性激活函數的函數圖像:
以下選擇了一個能夠模擬異或運算的數據集,不使用隱藏層,訓練500輪的分類效果。可以看出這個感知機模型無法將兩種不同顏色的點分開,也就是說感知機無法模擬異或運算。
而加入隱藏層之后,該模型很好的完成了分類任務,異或問題得到了解決。
從以上例子可以看出深層神經網絡實際上有組合特征提取的功能。這個特性對于解決不易提取特征向量的問題(比如圖片識別、語音識別等)有很大幫助。
4.2 損失函數定義
分類問題希望解決的是將不同的樣本分到事先定義好的類別中。通過神經網絡解決多分類問題最常用的方法是設置 n 個輸出節點,其中 n 為類別的個數。對于每一個樣例,神經網絡可以得到一個 n 維數組作為輸出結果。數組中的每一個維度對應一個類別。在理想情況下,如果一個樣本屬于類別 k,那么這個類別所對應的輸出節點的輸出值應該為 1,而其他節點的輸出都為 0。
交叉熵(cross entropy)是常用的評判一個輸出向量和期望的向量之間接近程度的損失函數。交叉熵刻畫了兩個概率分布之間的距離。
給定兩個概率分布 p 和 q,通過 q 來表示 p 的交叉熵為:
如果將分類問題中“一個樣例屬于一個類別”看成是一個概率事件,那么訓練數據的正確答案就符合一個概率分布。Softmax 回歸是一個非常常用的將神經網絡前向傳播得到的結果變成概率分布。Softmax 回歸本身可以作為一個學習算法來優化分類結果,但在 TensorFlow 中,Softmax 回歸的參數被去掉了,它只是一個額外的處理層,將神經網絡的輸出變成一個概率分布。
假設原始的神經網絡的輸出為 y1, y2, ····, yn,那么經過 Softmax 回歸處理之后的輸出為:
原始神經網絡的輸出被用作置信度來生成概率分布,就可以通過交叉熵來計算預測的概率分布和真實答案的概率分布之間的距離。
交叉熵函數不是對稱的( H(p, q) ≠ H(q, p) )。當交叉熵作為神經網絡的損失函數時,p 代表的是正確答案,q 代表的是預測值。交叉熵值越小,兩個概率分布越接近。
在計算交叉熵時,通常會用到以下幾個函數:
tf.clip_by_value(t, clip_value_min, clip_value_max, name=None):
輸入張量 t,把 t 中的元素的值都限制在 clip_value_min
和 clip_value_max
之間,小于 clip_value_min
的等于 clip_value_min
,大于 clip_value_max
的等于 clip_value_max
,這樣可以避免一些運算錯誤(比如 log0 無效)。
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
v = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
with tf.Session() as sess:
print(tf.clip_by_value(v, 2.5, 4.5).eval())
tf.log()
函數,對張量中所有元素依次求對數:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
v = tf.constant([[1.0, 2.0, 3.0]])
with tf.Session() as sess:
print(tf.log(v).eval())
*
操作,實現兩個矩陣對應元素直接相乘,矩陣乘法需要使用 tf.matmul()
函數,以下代碼給出了兩個操作的區別:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
v1 = tf.constant([[1.0, 2.0], [3.0, 4.0]])
v2 = tf.constant([[5.0, 6.0], [7.0, 8.0]])
with tf.Session() as sess:
print((v1 * v2).eval())
print(tf.matmul(v1, v2).eval())
tf.reduce_mean()
函數,用于計算張量 tensor 沿著指定的數軸(tensor的某一維度)上的平均值,主要用作降維或者計算 tensor 的平均值。
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
v = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
with tf.Session() as sess:
print(tf.reduce_mean(v).eval())
print(tf.reduce_mean(v, axis=0).eval())
print(tf.reduce_mean(v, axis=1).eval())
因為交叉熵一般會與 Softmax 回歸一起使用,所以 TensorFlow 對這兩個功能進行了統一封裝,并提供了
tf.nn.softmax_cross_entropy_with_logits()
函數。比如可以直接通過以下代碼來實現使用了 softmax 回歸之后的交叉熵損失函數:tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y)
其中 y 代表了原始神經網絡的輸出結果,而 y_ 給出了標準答案。
在只有一個正確答案的分類問題中,TensorFlow 提供了
tf.nn.sparse_softmax_cross_entropy_with_logits()
函數來進一步加速計算過程。
與分類問題不同,回歸問題解決的是對具體數值的預測。解決回歸問題的神經網絡一般只有一個輸出節點,輸出值就是預測值。對于回歸問題,最常用的損失函數是均方誤差(MSE,mean squared error),定義如下:
其中 yi 為一個 batch 中第 i 個數據的正確答案,而 yi' 為神經網絡給出的預測值。以下代碼展示了如何通過 TensorFlow 實現均方誤差損失函數:
mse = tf.reduce_mean(tf.square(y_ - y))
TensorFlow 不僅支持經典的損失函數,還可以優化任意的自定義損失函數。以預測商品銷量問題為例,一個商品的成本是 1 元,利潤是 10 元。為了最大化預期利潤,需要將損失函數和利潤直接聯系起來。注意損失函數定義的是損失,所以要將利潤最大化,定義的損失函數應該刻畫成本或者代價。以下公式給出了一個當預測多于真實值和預測少于真實值時有不同損失系數的損失函數:
在上面的問題中,a 等于 10(正確答案多于預測答案的代價), b 等于1(正確答案少于預測答案的代價)。在 TensorFlow 中,可以通過以下代碼來實現該損失函數:
loss = tf.reduce_sum(tf.where(tf.greater(v1, v2), (v1 - v2) * a, (v2 - v1) * b))
tf.greater()
輸入的是兩個張量,比較兩個輸入張量中每一個元素的大小,并返回比較結果。tf.where()
函數有三個參數:第一個作為選擇條件依據,當選擇條件為 True 時,選擇第二個參數中的值,否則使用第三個參數中的值。函數判斷和選擇都是在元素級別進行。以下代碼展示了
tf.where()
函數和 tf.greater()
函數的用法:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
v1 = tf.constant([1.0, 2.0, 3.0, 4.0])
v2 = tf.constant([4.0, 3.0, 2.0, 1.0])
with tf.Session() as sess:
print(tf.greater(v1, v2).eval())
print(tf.where(tf.greater(v1, v2), v1, v2).eval())
下面將通過一個簡單的神經網絡程序來講解損失函數對模型訓練結果的影響,在下面這個程序中,實現了一個擁有兩個輸入節點、一個輸出節點,沒有隱藏層的神經網絡:
import tensorflow as tf
from numpy.random import RandomState
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
batch_size = 8
# 定義兩個輸入節點
x = tf.placeholder(tf.float32, shape=(None, 2), name='x-input')
y_ = tf.placeholder(tf.float32, shape=(None, 1), name='y-input')
# 定義一個單層神經網絡前向傳播過程
w1 = tf.Variable(tf.random_normal([2, 1], stddev=1, seed=1))
y = tf.matmul(x, w1)
# 定義成本
loss_less = 10
loss_more = 1
loss = tf.reduce_sum(tf.where(tf.greater(y, y_), (y - y_) * loss_more, (y_ - y) * loss_less))
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)
# 隨機生成模擬數據集
rdm = RandomState(1)
dataset_size = 128
X = rdm.rand(dataset_size, 2)
# 設置回歸的預測值為兩個輸入的和加上隨機噪音(均值為0的小量,此處為-0.05~0.05的隨機數)
Y = [[x1 + x2 + rdm.rand() / 10.0 - 0.05] for (x1, x2) in X]
# 訓練神經網絡
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
STEPS = 5000
for i in range(STEPS):
start = (i * batch_size) % dataset_size
end = min(start + batch_size, dataset_size)
sess.run(train_step, feed_dict={x: X[start:end], y_: Y[start:end]})
print(sess.run(w1))
4.3 神經網絡優化算法
梯度下降算法(gradient descent)主要用于優化單個參數的取值,而反向傳播算法(backpropagation)給出了一個在所有參數上使用梯度下降算法的高效方式,從而使神經網絡模型在訓練數據上的損失盡可能小。反向傳播算法是訓練神經網絡的核心算法,它可以根據定義好的損失函數優化神經網絡中參數的取值,從而使神經網絡模型在訓練數據集上的損失函數達到一個較小值。
假設用 θ 表示神經網絡中的參數,J(θ) 表示在給定的參數取值下,訓練數據集上損失函數的大小,那么整個優化過程可以抽象為尋找一個參數 θ,使得 J(θ) 最小。因為目前沒有一個通用的方法可以對任意損失函數直接求解最佳的參數取值,所以在實踐中,梯度下降算法是最常用的神經網絡優化方法。梯度下降算法會迭代式更新參數 θ,不斷沿著梯度的反方向讓參數朝著總損失更小的方向更新。
參數的梯度可以通過求偏導的方式計算,對于參數 θ,其梯度為 :?J(θ)/?θ。
定義學習率 η (learning rate)來定義每次參數更新的幅度。通過參數的梯度和學習率,參數更新的公式為:
神經網絡的優化過程可以分為兩個階段,第一個階段先通過前向傳播算法計算得到預測值,并將預測值和真實值作對比得出兩者之間的差距。然后在第二個階段通過反向傳播算法計算損失函數對每一個參數的梯度,再根據梯度和學習率使用梯度下降算法更新每一個參數。
梯度下降算法并不能保證被優化的函數達到全局最優解。只有當損失函數為凸函數時,梯度下降算法才能保證達到全局最優解。
除了不一定能達到全局最優,梯度下降算法的另一個問題就是計算時間太長。因為要在全部訓練數據上最小化損失,所以損失函數 J(θ) 是在所有訓練數據上的損失和。這樣在每一輪迭代中都需要計算在全部訓練數據上的損失函數。
為了加速訓練過程,可以使用隨機梯度下降算法(stochastic gradient descent)。這個算法在每一輪迭代中,隨機優化某一條訓練數據的損失函數,大大加快每一輪參數更新的速度。缺點是:在某一條數據上損失函數更小并不代表在全部數據上損失函數更小,于是使用隨機梯度下降優化得到的神經網絡甚至可能無法達到局部最優。
綜合梯度下降算法和隨機梯度下降算法的優缺點,在實際應用中采用每次計算一小部分訓練數據的損失函數的方法。這一小部分訓練數據被稱之為一個 batch。通過矩陣運算,每次在一個 batch 上優化神經網絡的參數并不會比單個數據慢太多。另外,每次使用一個 batch 可以大大減小收斂所需要的迭代次數,同時可以使收斂的結果更加接近梯度下降的效果。以下代碼給出了在 TensorFlow 中如何實現神經網絡的訓練過程:
batch_size = n
# 每次讀取小部分訓練數據
x = tf.placeholder(tf.float32, shape=(batch_size, 2), name='x-input')
y_ = tf.placeholder(tf.float32, shape=(batch_size, 1), name='y-input')
# 定義神經網絡結構和優化算法
loss = ...
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)
# 訓練神經網絡
with tf.Session() as sess:
# 參數初始化
...
# 迭代的更新參數
for i in range(STEPS):
# 準備batch_size個訓練數據
current_X, current_Y = ...
sess.run(train_step, feed_dict={x: current_X, y_: current_Y})
4.4 神經網絡進一步優化
為了解決學習率的問題,TensorFlow 提供了一種靈活的學習率設置方法——指數衰減法(exponential attenuation/decay)。tf.train.exponential_decay()
函數實現了指數衰減學習率。通過這個函數,可以先使用較大的學習率來快速得到一個比較優的解,然后隨著迭代的繼續逐步減小學習率,使得模型在訓練后期更加穩定。exponential_decay
函數會指數級地減小學習率,它實現了以下代碼的功能:
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)
decay_rate
為衰減系數,decay_steps
為衰減速度,通常代表了完整的使用一遍訓練數據所需要的迭代輪數,也就是總訓練樣本數除以每個 batch 中的訓練樣本數。這種設置的使用場景是每完整過完一遍訓練數據,學習率就減小一次。這可以使得訓練數據集中的所有數據對模型訓練有相等的作用。下面給出的示例代碼展示了如何在 TensorFlow 中使用 tf.train.exponential_decay()
函數:
global_step = tf.Variable(0)
# 通過exponential_decay函數生成學習率
learning_rate = tf.train.exponential_decay(0.1, global_step, 100, 0.96, staircase=True)
# 使用指數衰減的學習率
learning_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
正則化(regularizaton)是非常常用的避免過擬合(overfitting)的方法。正則化的思想是在損失函數中加入刻畫模型復雜程度的指標。假設用于刻畫模型在訓練數據上表現的損失函數為 J(θ),那么在優化時不是直接優化 J(θ),而是優化 J(θ)+λR(w)。其中 R(w) 刻畫的是模型的復雜程度,而 λ 表示模型復雜損失在總損失中的比例。這里 θ 表示的是一個神經網絡中的所有參數,包括邊上的權重 w 和偏置項 b。一般來說模型復雜度只由權重 w 決定。
常用的刻畫模型復雜度的函數 R(w) 有兩種,一種是 L1 正則化,計算公式是:
另一種是 L2 正則化,計算公式是:
無論是哪種正則化方式,基本的思想都是希望通過限制權重的大小,使得模型不能任意擬合訓練數據中的隨機噪音。
L1 正則化會讓參數變得更稀疏,可以達到類似特征選取的方式。L1 正則化的計算公式不可導,而 L2 正則化公式可導。因為在優化時需要計算損失函數的偏導數,所有對含有 L2 正則化損失函數的優化要更加簡潔。
在實踐中,也可以將 L1 正則化和 L2 正則化同時使用:
以下代碼給出了一個簡單的帶 L2 正則化的損失函數定義:
w = tf.Variable(tf.random_normal([2, 1], stddev=1, seed=1))
y = tf.matmul(x, w)
loss = tf.reduce_mean(tf.square(y_ -y)) + tf.contrib.layers.l2_regularizer(lambda)(w)
loss 為定義的損失函數,由刻畫模型在訓練數據上表現的均方誤差損失函數和防止模型過度模擬訓練數據中的隨機噪音的 L2 正則化組成。lambda 參數表示了正則化項的權重,也就是公式 J(θ)+λR(w) 中的 λ。w 為需要計算正則化損失的參數。TensorFlow 提供了 tf.contrib.layers.l2_regularizer()
函數,它可以返回一個函數,這個函數可以計算一個給定參數的 L2 正則化項的值。類似的,tf.contrib.layers.l1_regularizer
可以計算 L1 正則化項的值。以下代碼給出了使用這兩個函數的樣例:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
weights = tf.constant([[1.0, -2.0], [-3.0, 4.0]])
with tf.Session() as sess:
print(sess.run(tf.contrib.layers.l1_regularizer(.5)(weights)))
print(sess.run(tf.contrib.layers.l2_regularizer(.5)(weights)))
當網絡結構復雜之后定義網絡結構的部分和計算損失函數的部分可能不在同一個函數中,這樣通過變量這種方式計算損失函數就不方便。為了解決這個問題,可以使用 TensorFlow 中提供的集合(collection)。它可以在一個計算圖中保存一組實體。以下代碼給出了通過集合計算一個 5 層神經網絡帶 L2 正則化的損失函數的計算方法:
import tensorflow as tf
from numpy.random import RandomState
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# 獲取一層神經網絡邊上的權重并將該權重的L2正則化損失加入集合中
def get_weight(shape, Lambda):
# 生成一個變量
var = tf.Variable(tf.random_normal(shape), dtype=tf.float32)
# 加入集合
tf.add_to_collection('losses', tf.contrib.layers.l2_regularizer(Lambda)(var))
# 返回生成的變量
return var
x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 1))
batch_size = 8
# 定義了每一層網絡中節點的個數
layer_dimension = [2, 10, 10, 10, 1]
# 定義神經網絡的層數
n_layers = len(layer_dimension)
# 定義變量,這個變量維護前向傳播時最深層的節點,開始的時候是輸入層
cur_layer = x
# 當前層的節點個數
in_dimension = layer_dimension[0]
for i in range(1, n_layers):
# layer_dimension[i]為下一層的節點個數
out_dimension = layer_dimension[i]
# 生成當前層中權重的變量并將L2正則化損失加入集合
weight = get_weight([in_dimension, out_dimension], 0.001)
bias = tf.Variable(tf.constant(0.1, shape=[out_dimension]))
# 使用ReLU激活函數
cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight) + bias)
# 將下一層的節點個數更新為當前層節點個數
in_dimension = layer_dimension[i]
# 計算損失函數
mse_loss = tf.reduce_mean(tf.square(y_ - cur_layer))
# 將均方誤差損失函數加入損失集合
tf.add_to_collection('losses', mse_loss)
# 返回列表
loss = tf.add_n(tf.get_collection('losses'))
滑動平均模型(moving average)可以使模型在測試數據上更健壯(robust)。在采用隨機梯度下降算法訓練神經網絡時,使用滑動平均模型在很多應用中都可以在一定程度上提高最終模型在測試數據上的表現。
TensorFlow 提供了 tf.train.ExponentialMovingAverage
來實現滑動平均模型。在初始化 ExponentialMovingAverage
時,需要提供一個衰減率(decay)。這個衰減率將用于控制模型更新的速度。ExponentialMovingAverage
對每一個變量會維護一個影子變量(shadow variable),這個影子變量的初始值就是對應變量的初始值,而每次運行變量更新時,影子變量的值會更新為:
shadow_variable = decay × shadow_variable + (1 - decay) × variable
decay
決定了模型更新的速度,decay
越大模型越趨于穩定。在實際應用中,decay
一般會設成非常接近 1 的數(比如 0.999 或 0.9999)。
如果在 ExponentialMovingAverage
初始化的時候提供了 num_updates
參數,那么每次使用的衰減率將是:
下面代碼解釋了
ExponentialMovingAverage
是如何被使用的:
import tensorflow as tf
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# 定義一個變量用于計算滑動平均,初始值為0
v1 = tf.Variable(0, dtype=tf.float32)
# step變量模擬神經網絡中迭代的輪數,用于動態控制衰減率
step = tf.Variable(0, trainable=False)
# 定義滑動平均的類,初始化時給定衰減率(0.99)和控制衰減率的變量step
ema = tf.train.ExponentialMovingAverage(0.99, step)
# 定義更新滑動變量的操作
maintain_averages_op = ema.apply([v1])
with tf.Session() as sess:
# 初始化變量
init_op = tf.global_variables_initializer()
sess.run(init_op)
# 通過ema.average(v1)獲取滑動平均之后變量的取值
print(sess.run([v1, ema.average(v1)]))
# 更新v1的值到5
sess.run(tf.assign(v1, 5))
# 更新v1的滑動平均值,衰減率為min{0.99, (1+step)/(10+step)=0.1}=0.1
sess.run(maintain_averages_op)
print(sess.run([v1, ema.average(v1)]))
# 更新step的值為0
sess.run(tf.assign(step, 10000))
# 更新v1的值為10
sess.run(tf.assign(v1, 10))
# 更新v1的滑動平均值,衰減率為min{0.99, (1+step)/(10+step)≈0.999}=0.99
sess.run(maintain_averages_op)
print(sess.run([v1, ema.average(v1)]))
# 再次更新滑動平均值
sess.run(maintain_averages_op)
print(sess.run([v1, ema.average(v1)]))