TensorFlow——深層神經網絡
深度學習與深層神經網絡
深度學習有兩個非常重要的特性——多層和非線性。
線性模型的侷限性
如果不使用激活函數,單純的用線性函數解決神經網絡的問題,那麼多層的網絡和單層的網絡並沒有區別,只是線性模型中的係數發生了變化。
也就是說,激活函數若採用線性的,那麼神經網絡就無法訓練出非線性適應模型,即與直接採用線性模型沒有差異。只有在激活函數是非線性的情況下,才能用神經網絡解決非線性問題。

以下代碼展示瞭如何通過激活函數實現神經網絡的前向傳播算法。
a = tf.nn.relu(tf.matmul(x, w1) + biases1)
y = tf.nn.relu(tf.matmul(a, w2) + biases2)
損失函數定義
經典損失函數
交叉熵是常用的評判方法之一。交叉熵刻畫了兩個概率分佈之間的距離,它是分類問題中使用比較廣的一種損失函數。
給定兩個概率分佈p和q,通過q來表示p的交叉熵爲:
將神經網絡前向傳播得到的結果變爲概率分佈的一個常用方式是使用Softmax迴歸。
Softmax迴歸本身可以作爲一個學習算法來優化分類結果,但在TensorFlow中,Softmax迴歸的參數被去掉了,它只是一層額外的處理層,將神經網絡的輸出變成一個概率分佈。如圖所示。

假設原始的神經網絡輸出爲 ,那麼經過Softmax迴歸處理之後的輸出爲:
從上述公式中可以看出,原始神經網絡的輸出被用作置信度來生成新的輸出,而新的輸出滿足概率分佈的所有要求。
因爲正確答案是希望得到的結果,所以當交叉熵作爲神經網絡的損失函數時,p代表的是正確答案,q代表的是預測值,交叉熵值越小,兩個概率分佈越接近。
那麼,通過TensorFlow實現交叉熵的代碼爲:
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))
其中y_代表正確結果,y代表預測結果,通過tf.clip_by_value函數可以將一個張量中的數值限制在一個範圍之內,這樣可以避免一些運算錯誤(比如log0是無效的)。
一個tf.clip_by_value的代碼樣例:
v = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print tf.clip_by_value(v, 2.5, 4.5).eval()
#輸出[[2.5 2.5 3] [4 4.5 4.5]]
即,小於2.5的數都被換成了2.5,大於4.5的數都被換成了4.5。
第二個函數是tf.log函數,這個函數完成了對張量中所有元素依次求對數的過程。
第三個運算是乘法,在實現交叉熵的代碼中,直接將兩個矩陣通過”*“操作相乘,這個操作不是矩陣乘法,而是元素之間相乘,矩陣乘法需要使用tf.matmul函數來完成。
tf.reduce_mean函數是對整個矩陣做平均
v = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print tf.reduce_mean(v).eval()
#輸出3.5
因爲交叉熵一般會與softmax迴歸一起使用,所以TensorFlow對這兩個功能進行了統一封裝,並且提供了tf.nn.softmax_cross_entropy_with_logits函數。比如可以直接通過下面的代碼來實現使用了softmax迴歸之後的交叉熵損失函數:
#只能使用命名參數的方式來調用
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=y, labels=y_)
而對於只有一個正確答案的分類問題中,TensorFlow提供了tf.nn.sparse_softmax_cross_entropy_with_logits函數來進一步加快計算過程。
自定義損失函數
TensorFlow還支持任意的自定義損失函數。
比如下面的公式給出了一個當預測多餘真實值和預測少於真實值時有不同損失係數的損失函數。
其中
在TensorFlow中,可以通過下列代碼實現這個損失函數。
loss = tf.reduce_sum(tf.select(tf.greater(v1, v2), (v1 - v2) * a, (v2 - v1) * b))
tf.greater的輸入是兩個張量,此函數會比較這兩個輸入張量中每一個元素的大小,並返回比較結果。
tf.select函數有三個參數。第一個爲選擇條件根據,當選擇條件爲True時,tf.select函數會選擇第二個參數中的值,否則使用第三個參數中的值。
要注意,tf.select函數判斷和選擇都是在元素級別進行
import tensorflow as tf
v1 = tf.constant([1.0, 2.0, 3.0, 4.0])
v2 = tf.constant([4.0, 3.0, 2.0, 1.0])
sess = tf.InteractiveSession()
print tf.greater(v1, v2).eval()
#輸出[Flase, False, True, True]
print tf.select(tf.greater(v1, v2), v1, v2).eval()
#輸出[4. 3. 3. 4.]
sess.close()
神經網絡進一步優化
介紹了神經網絡的基本算法之後,我們還可以針對優化過程中可能遇到的一些問題提出一些常用的解決方法。
1. 通過指數衰減的方法設置梯度下降算法中的學習率。通過指數衰減的學習率即可以讓模型在訓練的前期快速接近較優解,又可以保證模型在訓練後期不會有太大的波動,從而更加接近局部最優。
2. 過擬合問題的具體解決方法
3. 滑動平均模型。滑動平均模型會將每一輪迭代得到的模型綜合起來,從而使得最終得到的模型更加健壯(robust)。
指數衰減學習率
TensorFlow提供了一種更加靈活的學習率設置方法——指數衰減法。tf.train.exponential_decay
函數實現了指數衰減學習率。
exponential_decay函數會指數級地減小學習率,它實現了以下代碼的功能。
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)
decayed_learning_rate爲每一輪優化時使用的學習率,learning_rate爲事先設定的初始學習率,decay_rate爲衰減係數,decay_steps爲衰減速度。
tf.train.exponential_decay函數可以通過設置參數staircase選擇不同的衰減方式。staircase的默認值爲False,這時候學習率迭代輪數變化的趨勢爲連續的。當staircase的值被設置爲True時, global_step / decay_steps會被轉化成整數。這使得學習率成爲一個階梯函數。在這樣的設置下,decay_steps通常代表了完整的使用一遍訓練數據所需要的迭代輪數,這個迭代輪數也就是總訓練樣本數除以每一個batch中的訓練樣本數。
global_step = tf.Variable(0)
learning_rate = tf.train.exponential_decay(0.1, globla_step, 100, 0.96, staircase=True)
#使用指數衰減的學習率。在minimize函數中傳入global_step將自動更新global_step參數,從而使得學習率也得到相應更新。
learning_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(...my loss..., global_step=global_step)
過擬合問題
爲了避免過擬合問題,常用方法是正則化。正則化的思想就是在損失函數中加入刻畫模型複雜程度的指標。
常用的刻畫模型複雜程度的函數R(w)有兩種,一種是L1正則化,計算公式是:
另一種是L2正則化,計算公式是:
這兩種正則化方法有很大區別。首先,L1正則化會讓參數變得更稀疏,而L2正則化不會。所謂參數變得更稀疏是指會有更多的參數變爲0,這樣可以達到類似特徵選取的功能。之所以L2正則化不會讓參數變得稀疏的原因是當參數很小時,比如0.001,這個參數的平方基本上就可以忽略了,於是模型不會進一步將這個參數調整爲0.其次,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)
TensorFlow提供了tf.contrib.layers.l2_regularizer函數,它可以返回一個函數,這個函數可以計算一個給定參數的L2正則化項的值。lambda參數表示了正則化項的權重,w爲需要計算正則化損失的參數。
類似的,tf.contrib.layers.l1_regularizer可以計算L1正則化項的值。
但是,當神經網絡的參數增多之後,這樣的方式首先可能導致損失函數loss的定義很長,可讀性差且容易出錯。當網絡結構複雜之後定義網絡結構的部分和計算損失函數的部分可能不在同一個函數中,這樣通過變量這種方式計算損失函數就不方便了。於是我們可以使用TensorFlow中提供的集合概念。
以下代碼給出通過集合計算一個五層神經網絡帶L2正則化的損失函數的計算方法。
import tensorflow as tf
#獲取一層神經網絡的權重,並將L2損失加入到loss的集合中
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):#循環隨機產生每一層的權重
out_dimension = layer_dimension[i]
weight = get_weight([in_dimension, out_dimension], 0.001)
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'))## 最終的損失函數
滑動平均模型
在TensorFlow中提供了tf.train.ExponentialMovingAverage來實現滑動平均模型。
在初始化ExponentialMovingAverage時,需要提供一個衰減率decay。這個衰減率將用於控制模型更新的速度。ExponentialMovingAverage對每一個變量會維護一個影子變量,這個影子變量的初始值就是相應變量的初始值,而每次運行變量更新時,影子變量的值會更新爲:
shadow_variable = decay × shadow_variable + (1 - decay) × variable
shadow_variable爲影子變量,variable爲更新後的變量值,decay爲衰減率。
decay決定了模型更新的速度,decay越大模型越趨於穩定。
爲了使得模型在訓練前期可以更新得更快,ExponentialMovingAverage還提供了num_updates參數來動態設置decay的大小。如果在ExponentialMovingAverage初始化時提供了num_updates參數,那麼每次使用的衰減率將是
min{decay,1+num_updates/10+num_updates}
下面通過代碼來理解滑動平均模型
import tensorflow as tf
v1 = tf.Variable(0, dtype = tf.float32)#定義一個變量用於計算滑動平均,初始值爲0
step = tf.Variable(0, trainable=Flase)#step變量模擬神經網絡迭代次數用於動態控制衰減率)
ema = tf.train.ExponentialMovingAverage(0.99, step)#定義滑動平均的類
maintain_averages_op = ema.apple([v1])
with tf.Session() as sess:
init_op = tf.initialize_all_avriables()
sess.run(init_op)
#通過ema.average獲取滑動平均之後變量的取值。
print sess.run([v1, ema.average(v1)])#輸出[0.0, 0.0]
sess.run(tf.assign(v1, 5))#更新v1的值到5
#更新v1的滑動平均值。衰減率爲min{0.99, (1+step)/(10+step)=0.1}=0.1
#所以v1的滑動平均會被更新爲0.1×0+0.9×5=4.5
sess.run(maintain_averages_op)
print sess.run([v1, ema.average(v1)])#輸出[5.0, 4.5]