TensorFlow深層神經網絡

學習筆記,內容和代碼來自書《TensorFlow實戰Google深度學習框架》

目錄

深度學習與深層神經網絡

使用激活函數實現去線性化

如何選擇激活函數

多層網絡解決異或運算

損失函數

經典損失函數

自定義損失函數

神經網絡優化算法

梯度下降算法

神經網絡進一步優化

學習率的設置

過擬合問題

滑動平均模型


深度學習與深層神經網絡

深度學習是一類通過多層非線性變換對高複雜性數據建模算法的合集。                          ——維基百科

 而深層神經網絡是實現“多層非線性變換”的最常用方法,所以基本上可以認爲深度學習是深層神經網絡的代名詞。

在之前的對於神經網絡的介紹中,採用了前向傳播算法,是一個線性模型,線性模型的特點是任意線性模型的組合都是一個線性變換,這樣即使神經網絡有多層,他和單層神經網絡也沒有什麼區別。這也正是線性模型的侷限之處,也是爲什麼深度學習要強調非線性。

使用線性模型解決線性可分問題的效果:

使用激活函數實現去線性化

神經網絡遊樂場中,激活函數爲上面的Activation選項,有Linear,ReLU,Tanh,Sigmoid幾個選項。

如果在神經元中的輸出通過一個非線性函數,那麼神經網絡模型也不再是線性的了,這個非線性函數就是激活函數。

函數圖像和介紹來自博客:TensorFlow神經網絡中的激活函數 from Jean_V

常見的幾種激活函數:

                                                                                             Sigmoid函數

                                                                                               Tanh函數

  • tanh與sigmoid非常接近,且與後者具有類似的優缺點,
  • sigmoid和tanh的主要區別在於tanh的值爲[-1.0,1.0]優點在於在一些特定的網絡架構中,能夠輸出負值的能力十分有用。
  • 缺點在於注意tanh值域的中間點爲0.0,當網絡中的下一層期待輸入爲負值或者爲0.0時,這將引發一系列問題。

                                                               Relu(Rectified Linear Units修正線性單元)函數

                                                                                          線性函數

線性函數即爲y=kx+b

通過激活函數後,每一個神經元都不再是線性變換。目前TensorFlow提供7種不同的非線性激活函數,tf.nn.relu、tf.sigmoid和tf.tanh是比較常用的幾個,調用語法如下:

a = tf.nn.relu(tf.matmul(x, w1) + biases1)
y = tf.nn.relu(tf.matmul(a, w2) + biases2)

如何選擇激活函數

轉自博客:TensorFlow神經網絡中的激活函數 from Jean_V

激活函數好或壞,不能憑感覺定論。然而,根據問題的性質,我們可以爲神經網絡更快更方便地收斂作出更好的選擇。

用於分類器時,Sigmoid函數及其組合通常效果更好。

由於梯度消失問題,有時要避免使用sigmoid和tanh函數。

ReLU函數是一個通用的激活函數,目前在大多數情況下使用。

如果神經網絡中出現死神經元,那麼PReLU函數就是最好的選擇。

請記住,ReLU函數只能在隱藏層中使用。

一點經驗:你可以從ReLU函數開始,如果ReLU函數沒有提供最優結果,再嘗試其他激活函數。

多層網絡解決異或運算

感知機(perceptron)模型從數學上完成了對神經網絡的建模,感知機可以理解爲單層神經網絡。感知機會先將輸入進行加權和,然後通過激活函數得到輸出,這就是一個沒有隱藏層的神經網絡。根據相關論文的數學推導,感知機是無法模擬異或運算的,如下圖。

加入隱藏層後疑惑問題得到解決,如下圖:

隱藏層的四個節點中,每個節點都有一個角是黑色的,這四個隱藏節點可以被認爲代表了從輸入特徵中抽取更高維的特徵。

損失函數

經典損失函數

分類問題和迴歸問題是監督學習的兩大種類,這一部分重點介紹分類問題和迴歸問題的經典損失函數。

分類問題希望解決的是將不同的樣本分到事先定義好的類別中。通過神經網絡解決多分類問題通常方法是設置n個輸出節點,n爲類別個數。對於每一個樣例,神經網絡得到一個n維數組作爲輸出結果,如果一個樣本屬於類別k,那麼對應輸出節點的輸出值應該爲1,如識別數字1,結果應該越接近[0,1,0,0,0,0,0,0,0,0]越好。如何判斷識別輸出向量和期望向量有多接近呢?交叉熵是常用的(corss entropy)是常用的評判方法之一。交叉熵刻畫了兩個概率分佈之間的距離,是常用的一種損失函數。

交叉熵是一個信息論中的概念,原本用於計算平均編碼長度,計算:

給定兩個概率p,q,通過q來表示p的交叉熵爲

H\left ( p,q \right )=-\sum p\left ( x \right )\log q\left ( x \right )

交叉熵刻畫的是兩個概率分佈之間的距離,然而神經網絡輸出卻不一定是一個概率分佈(即要求概率均大於0且概率之和必須是1)。所以經常使用Softmax迴歸是一個常用的方法,將神經網絡前向傳播的結果變成概率分佈。

概率論領域中,Softmax函數又稱歸一化函數。假設神經網絡原始輸出爲y_1,y_2,...,y_n,那麼經過Softmax迴歸處理之後輸出爲:

softmax\left ( y \right )_i=y_i'=\frac{e^{y_{i}}}{\sum_{j=1}^{n}e_{j}}

這樣就可以通過交叉熵來計算預測的概率分佈和真實答案的概率分佈之間的距離了,也就是說交叉熵越小,兩個概率分佈越接近。

TensorFlow中實現交叉熵代碼如下:

cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))

其中,y_代表正確結果,y代表預測結果。

因爲交叉熵一般和Softmax迴歸一起使用,所以TensorFlow對這兩個功能統一封裝,提供了tf.nn.softmax_cross_entropy_with_logits() 

例如使用softmax迴歸後的交叉熵損失函數:

cross_entropy=tf.nn.softmax_cross_entropy_with_logits(y,y_)

y代表原始神經網絡的輸出結果,y_指的是正確答案。通過這樣一個命令就可以得到使用了softmax迴歸之後的交叉熵。在只有一個正確答案的分類問題中,tf提供了tf.nn.sparse_softmax_cross_entropy_with_logits()函數來進一步加速計算過程(後面會用到)。

與分類問題不同,迴歸問題需要解決的是對具體數值的預測。這些問題需要預測的不是一個實現定義好的類別,而是一個任意實數。解決迴歸問題的神經網絡一般只有一個輸出節點,這個節點的輸出值就是預測值。對於迴歸問題,最常用的損失函數是均方誤差(MSE,mean squared error),定義如下:

MSE\left ( y,y_{\_} \right )=\frac{\sum_{i=1}^{n}(y_i-y_i')^2}{n}

其中y_i爲一個batch中第i個數據的正確答案,而y_i'爲神經網絡給出的預測值。

使用TensorFlow實現均方誤差損失函數:

mse=tf.reduce_mean(tf.square(y_-y))

其中,y代表了神經網絡的輸出答案,y_代表了標準答案。 

自定義損失函數

tf不僅支持經典的損失函數,還可以優化任意的自定義損失函數,使得神經網絡優化的結果更接近實際需求。

爲了最大化預期利潤,需要將損失函數和利潤直接聯繫起來,損失函數定義的是損失。要將利潤最大化,定義的損失函數應該刻畫成本或者代價。 

例如,一個產品成本一元,利潤10元,那麼少預測一個就少賺10元,多預測一個少賺1元,如果神經網絡模型是最小化的均方誤差,就無法預測最大化收益,我們可以定義下列函數:

Loss(y,y')=\sum_{i=1}^{n}f(y_i,y_i'),f(x,y)=\left\{\begin{matrix} a(x-y) &x>y\\ b(y-x) &x\leq y \end{matrix}\right.

TensorFlow代碼實現:

loss=tf.reduce_sum(tf.where(tf.greater(v1,v2),(v1-v2)*a,(v2-v1)*b))

tf.greater的輸入是兩個張量,此函數會比較這兩個輸入張量中每一個元素的大小,並返回比較結果。當張量維度不一樣時,TensorFlow會進行類似NumPy廣播(broadcasting)操作。第一個爲選擇條件依據,True時,tf.where函數會選擇第二個參數中的值,False使用第三個參數中的值。 

用法:

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])

sees = tf.InteractiveSession()
print tf.greater(v1, v2).eval()
#輸出[false flase true true]

print tf.where(tf.greater(v1, v2), v1, v2).eval()
#輸出[4. 3. 3. 4.],類似?=運算符
sees.close

神經網絡優化算法

具體介紹通過反向傳播算法(backpropagation)梯度下降算法(gradient decent)調整參數取值。梯度下降算法主要用於優化單個參數的取值,而反向傳播算法給出了一個高效的方式在所有參數上使用梯度下降算法,使模型在訓練數據上損失函數儘可能小。

梯度下降算法

梯度下降算法是最常用的神經網絡優化方法。參數的梯度可以通過求偏導實現,對於參數\theta,梯度爲\frac{\partial J\left ( \theta \right )}{\partial \theta }。有了梯度,還需要定義一個學習率\eta(learning rate)來定義每次參數更新的幅度。

從直觀上理解,可以認爲學習率定義的就是每次參數移動的幅度。通過參數的梯度和學習率,參數的新公式爲\theta _{n+1}=\theta _n-\eta \frac{\partial J(\theta _n)}{\partial \theta _n}

神經網絡的優化過程可以分爲兩個階段:

  1. 通過前向傳播算法得到預測值,並將預測值和真實值做對比得到兩者之間的差距
  2. 通過反向傳播算法計算損失函數對每一個參數的梯度,再根據梯度和學習率使用梯度下降法更新每一個參數。

注意:梯度下降算法並不能保證被優化的函數達到全局最優解。在訓練神經網絡時,參數的初始值會很大程度影響最後的結果。只有當損失函數是凸函數時,梯度下降算法才能達到全局最優解。

另外,梯度下降算法還有一個問題是訓練時間過長。爲了加速訓練過程,可以使用隨機梯度下降算法(stochastic gradient descent)。這個算法不是在全部訓練數據上的損失函數,而是在每一輪迭代中,隨機優化某一條訓練數據上的損失函數,這樣速度就大大加快了。它的問題在於,某條數據上的損失函數更小並不代表全部數據上的損失函數最小,因此使用這種方法可能甚至達不到局部最優。

所以,一般採用這兩種方法的折中——每次計算一小部分訓練數據的損失函數,稱之爲batch。

下面是TensorFlow中實現神經網絡的訓練過程:

import tensorflow as tf

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:
    tf.global_variables_initializer().run() # 參數初始化
    # 迭代更新參數
    for i in range(Steps):
        # 準備batch_size個訓練數據,一般將所有訓練數據隨機打亂後再選取可以得到
        # 更好的優化效果
        sess.run(train_step,feed_dict={x:X[start:end],y_:Y[start:end]})

神經網絡進一步優化

  • 指數衰減的方法設置梯度下降算法的學習率(調整的步長)
  • 過擬合問題
  • 滑動平均模型

學習率的設置

學習率決定了參數每次更新的幅度,幅度如果過大會導致參數在極優值兩側來回移動。相反,學習率過小會大大降低優化速度。因此學習率不能過大也不能太小。

爲了解決學習率的問題,TensorFlow提供了一種更加靈活的學習率設置方法——指數衰減法。tf.train.exponential_decay函數實現了指數衰減學習率。它可以先使用較大的學習率來快速獲得一個比較優的解,然後隨着迭代的繼續逐步減小學習率,使得模型在訓練後期更加穩定。

tf.train.exponential_decay可以通過設置參數staircase選擇不同的衰減方式。staircase默認值False,設置爲True時,學習率會成爲一個階梯函數(staircase function)。 

代碼實現:

decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)

其中,decayed_learning_rate爲每一輪優化時使用的學習率,learning_rate爲事先的學習率,decay_steps爲衰減速度。

示例代碼:TensorFlow中使用tf.train.exponential_decay函數:

global_step=tf.Variable(0)

# 通過exponential_decay函數生成學習率
# learning_rate = tf.train.exponential_decay(初始學習率, global_step, 輪數, 學習率乘的倍數(0.96), staircase=True)
learning_rate = tf.train.exponential_decay(0.1,global_step,100,0.96,staircase=True)

# 使用指數衰減的學習率。在,minmize函數中傳入global_step將自動更新global_step參數,使學習率額更新
learning_step = tf.train.GradientDecentOptimizer(learning_rate).minmize(...my loss..., global_step=global_step)

上面設置初始學習率爲0.1,因爲staircase=True,即學習率爲階梯函數,所以每訓練100輪後學習率乘以0.96。一般,學習率的初始值、衰減係數和衰減速度都是根據經驗設置。而且,損失函數下降速度和迭代結束後總損失沒必然聯繫,不能通過前幾輪損失函數下降的速度來比較不同神經網絡的效果。

過擬合問題

在真實的應用中,希望通過訓練出來的模型對未知的數據給出判斷。過擬合指的是模型很好的記憶了每一個訓練數據中隨機噪聲的部分而忘記了要去學習訓練數據中通用的趨勢。 

即:過度擬合訓練數據中的隨機噪聲雖然可以得到非常小的損失函數,但是對於未知數據可能無法做出可靠的判斷 。

爲了避免過擬合問題,一個常用方法是正則化(regularization)。正則化的思想就是在損失函數中加入刻畫模型複雜度的指標。刻畫模型複雜度的函數有兩種:

L1正則化:

R(w)=\left \| w \right \|_1=\sum_{i}^{ }\left | w_i \right |

L2正則化:

R(w)=\left \| w \right \| _2 ^2=\sum_{i}^{ }\left | w_i ^2 \right |

兩種正則化方法都是希望通過限制權重的大小,使得模型不能任意擬合訓練數據中的隨機噪音。實踐中,常將兩種正則化方法結合起來使用:

R(w)=\sum_{i}^{ }\alpha \left | w_i \right |+(1-\alpha )w_i^2

TensorFlow帶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爲定義的損失函數,有兩個部分組成。第一個部分是均方誤差損失函數;第二個部分是正則化,防止模型過度模擬訓練數據中的隨機噪聲。lambda參數表示了正則化項的權重,也就是\lambdaw爲需要計算正則化損失的參數。

TensorFlow提供了tf.contrib.layers.l2_regularizer函數,這個函數可以計算一個給定參數的L2正則化項的值。tf.contrib.layers.l1_regularizer函數可以計算L1正則化項的值。 

樣例:

import tensorflow as tf

weights=tf.constant([[1.0,2.0],[3.0,4.0]])
with tf.Session() as sess:
    # 輸出爲(|1|+|-2|+|-3|+|4|)×0.5=5。0.5爲正則化項的權重。
    print(sess.run(tf.contrib.layers.l1_regularizer(.5)(weights)))
    # 輸出爲(1^2+(-2)^2+(-3)^2+4^2)/2*0.5=7.5
    print(sess.run(tf.contrib.layers.l2_regularizer(.5)(weights)))

上述過程可以計算簡單的神經網絡中的正則化的損失函數。但是當神經網絡中的參數增多之後,可能會導致損失函數loss的定義很長,可讀性差而且容易出錯。更糟糕的是,當網絡結構變複雜後,定義網絡結構的部分和計算損失函數的部分可能不在同一個函數中,這樣通過變量這種方式計算損失函數就不方便了。 
此時,可以通過tf提供的集合。集合可以在一個計算圖(tf.Graph)中保存一組實體(比如張量)。
樣例:通過集合計算一個5層神經網絡帶L2正則化的損失函數的計算方法:

import tensorflow as tf

#獲取一層神經網絡邊上的權重,並將這個權重的L2正則化損失加入名稱爲'losses'的集合中
def get_weight(shape,lambda1):
    #生成一個變量
    var=tf.Variable(tf.random_normal(shape),dtype=tf.float32)
    #add_to_collection 函數將這個新生成變量的L2正則化損失項加入集合
    #這個函數第一個參數losses是集合的名字,第二個參數是要加入這個集合的內容
    tf.add_to_collection('losses',tf.contrib.layers.l2_regularizer(lambda1)(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,5,3,1]
n_layers=len(layer_dimension)

#這個變量維護前向傳播時最深層的節點,開始的時候就是輸入層
cur_layer=x
# 當前層的節點個數
in_dimension=layer_dimension[0]

#通過一個循環來生成5層全連接的神經網絡結構
for i in range(1,n_layers):
    # layer_demension[i]爲下一層的節點數
    out_dimension=layer_dimension[i]
    #生成當前層中權重的變量,並將這個變量的L2正則化損失加入計算圖上的集合
    weight=get_weight([in_dimension,out_dimension],0.003)
    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]

#在定義神經網絡前向傳播的同時已經將所有的L2正則化損失加入了圖上的集合,
#這裏只需要計算刻畫模型在訓練數據上表現的損失函數
mse_loss=tf.reduce_mean(tf.square(y_-cur_layer))

#將均方誤差損失函數加入損失集合
tf.add_to_collection('losses',mse_loss)

#get_collection返回一個列表,這個列表是所有這個集合中的元素。在這個樣例中,
#這些元素就是損失函數的不同部分,將它們加起來就可以得到最終的損失函數
loss=tf.add_n(tf.get_collection('losses'))

滑動平均模型

在採用隨機梯度下降算法訓練神經網絡時,使用滑動平均模型在很多應用中都可以提高性能。 
tf中提供了tf.train.ExponentialMovingAverage來實現滑動平均模型。初始化這個函數時,需要提供一個衰減率。衰減率決定了模型更新的速度,衰減率越大模型越穩定。一般設爲0.999或者0.9999 (非常接近於1的數)。
樣例:ExponentialMovingAverage如何被使用。

import tensorflow as tf

# 定義一個變量用於計算滑動平均,這個變量的初始值爲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:
    tf.global_variables_initializer().run()
    #通過ema.average(v1)獲取滑動平均之後變量的取值。在初始化之後變量v1的值和v1的滑動平均都爲0
    print(sess.run([v1,ema.average(v1)]))

    #更新變量v1的值爲5
    sess.run(tf.assign(v1, 5))
    #更新v1的滑動平均值。衰減率爲min(0.99,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]

    #更新step的值爲10000
    sess.run(tf.assign(step, 10000))
    #更新v1的值爲10
    sess.run(tf.assign(v1, 10))
    #更新v1的滑動平均值。衰減率爲min(0.99,(1+10000)/(10+10000))=0.99
    #v1的滑動平均會被更新爲0.99*4.5+0.01*10=4.555
    sess.run(maintain_averages_op)
    print(sess.run([v1,ema.average(v1)]))
    #[10.0, 4.5549998]

    #再次更新滑動平均值,得到的新滑動平均值爲 0.99*4.555+0.01*10=4.60945
    sess.run(maintain_averages_op)
    print(sess.run([v1,ema.average(v1)]))
    #[10.0, 4.6094499]

後面會給出真實使用樣例。

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