神經(jīng)網(wǎng)絡(luò)(四):應(yīng)用示例之分類

一、 傳統(tǒng)分類模型的局限

在之前的文章中(《神經(jīng)網(wǎng)絡(luò)(一)》、《神經(jīng)網(wǎng)絡(luò)(二)》《神經(jīng)網(wǎng)絡(luò)(三)》),我們討論的重點(diǎn)是神經(jīng)網(wǎng)絡(luò)的理論知識?,F(xiàn)在來看一個實(shí)際的例子,如何利用神經(jīng)網(wǎng)絡(luò)解決分類問題。(為了更好地展示神經(jīng)網(wǎng)絡(luò)的特點(diǎn),我們在這個示例中并不劃分訓(xùn)練集和測試集)。

分類是機(jī)器學(xué)習(xí)最常見的應(yīng)用之一,之前的章節(jié)也討論過很多解決分類問題的機(jī)器學(xué)習(xí)模型,比如邏輯回歸和支持向量學(xué)習(xí)機(jī)等。但這些模型最大的局限性是它們都有比較明確的適用范圍,如果訓(xùn)練數(shù)據(jù)符合模型的假設(shè),則分類效果很好。否則,分類的效果就會很差。

比如圖1[1]中展示了4種不同分布類型的數(shù)據(jù)。具體來說,數(shù)據(jù)里有兩個自變量,分別對應(yīng)著坐標(biāo)系的橫縱軸;數(shù)據(jù)分為兩類,在圖中用三角形表示類別0,用圓點(diǎn)表示類別1。如果使用邏輯回歸對數(shù)據(jù)進(jìn)行分類,只有圖中標(biāo)記1中的模型效果較好(圖中的灰色區(qū)域里,模型的預(yù)測結(jié)果是類別0;白色區(qū)域里,模型的預(yù)測結(jié)果是類別1),因?yàn)樵谝阎悇e的情況下,數(shù)據(jù)服從正態(tài)分布(不同類別,分布的中心不同),符合邏輯回歸的模型假設(shè)。對于標(biāo)記2、3、4中的數(shù)據(jù),由于類別與自變量之間的關(guān)系是非線性的,如果想取得比較好的分類效果,則需要其他的建模技巧。比如先使用核函數(shù)對數(shù)據(jù)進(jìn)行升維,再使用支持向量學(xué)習(xí)機(jī)進(jìn)行分類。

圖1

二、 神經(jīng)網(wǎng)絡(luò)的優(yōu)勢

這樣的建模方法是比較辛苦的,要求搭建模型的數(shù)據(jù)科學(xué)家對不同模型的假設(shè)以及優(yōu)缺點(diǎn)有比較深刻的理解。但如果使用神經(jīng)網(wǎng)絡(luò)對數(shù)據(jù)進(jìn)行分類,則整個建模過程就比較輕松了,只需設(shè)計(jì)神經(jīng)網(wǎng)絡(luò)的形狀(包括神經(jīng)網(wǎng)絡(luò)的層數(shù)以及每一層里的神經(jīng)元個數(shù)),然后將數(shù)據(jù)輸入給模型即可。
在這個例子中,使用的神經(jīng)網(wǎng)絡(luò)如圖2所示,是一個3-層的全連接神經(jīng)網(wǎng)絡(luò)。

圖2

使用這個神經(jīng)網(wǎng)絡(luò)對數(shù)據(jù)進(jìn)行分類,得到的結(jié)果如圖3所示,可以看到同一個神經(jīng)網(wǎng)絡(luò)(結(jié)構(gòu)相同,但具體的模型參數(shù)是不同的)對4種不同分布類型的數(shù)據(jù)都能較好地進(jìn)行分類。

圖3

三、 代碼實(shí)現(xiàn)(完整的代碼請見)

這一節(jié)節(jié)將討論如何借助第三方庫TensorFlow來實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò),。

第一步是定義神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu),如程序清單1所示。

  1. 我們使用類(class)來實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò),如第4行代碼所示。在Python的類中可以定義相應(yīng)的函數(shù),但在類中,函數(shù)的定義與普通函數(shù)的定義有所不同,它的參數(shù)個數(shù)必須大于1,且第一個參數(shù)表示類本身,如第7行代碼里的“self”變量。但在調(diào)用這個函數(shù)時,卻不需要“手動”地傳入這個參數(shù),Python會自動地進(jìn)行參數(shù)傳遞,比如defineANN函數(shù)的調(diào)用方式是“defineANN()”。
  2. 在ANN類中,“self.input”對應(yīng)著訓(xùn)練數(shù)據(jù)里的自變量(它的類型是tf.placeholder),如第12行代碼所示,“self.input.shape[1].value”表示輸入層的神經(jīng)元個數(shù)(針對如圖2的神經(jīng)網(wǎng)絡(luò),這個值等于2)。而“self.size”是表示神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)的數(shù)組(針對如圖2的神經(jīng)網(wǎng)絡(luò),這個值等于[4, 4, 2])。在ANN類中,“self.input”對應(yīng)著訓(xùn)練數(shù)據(jù)里的自變量(它的類型是tf.placeholder),如第12行代碼所示,“self.input.shape[1].value”表示輸入層的神經(jīng)元個數(shù)(針對如圖12-8的神經(jīng)網(wǎng)絡(luò),這個值等于2)。而“self.size”是表示神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)的數(shù)組(針對如圖2的神經(jīng)網(wǎng)絡(luò),這個值等于[4, 4, 2])。
  3. 接下來是定義網(wǎng)絡(luò)的隱藏層。首先是神經(jīng)元里的線性模型部分,如第18~21行代碼所示,定義權(quán)重項(xiàng)“weights”和截距項(xiàng)“biases”。因此,權(quán)重項(xiàng)是一個的矩陣,而截距項(xiàng)是一個維度等于的行向量。值得注意的是,在定義權(quán)重項(xiàng)時,使用tf.truncated_normal函數(shù)(近似地對應(yīng)著正態(tài)分布)來生成初始值,在生成初始值的過程中,我們用如下的命令來規(guī)定分布的標(biāo)準(zhǔn)差“stddev=1.0 / np.sqrt(float(prevSize))”,這樣操作的原因是為了使神經(jīng)網(wǎng)絡(luò)更快收斂。定義好線性模型后,就需要定義神經(jīng)元的激活函數(shù),如第22行代碼所示,使用的激活函數(shù)是tf.nn.sigmoid,它對應(yīng)著sigmoid函數(shù)。
  4. 最后是定義神經(jīng)網(wǎng)絡(luò)的輸出層,如第25~29行代碼所示。具體的過程和隱藏層類似,唯一不同的是,輸出層并沒有激活函數(shù),因此只需定義線性模型部分“tf.matmul(prevOut, weights) + biases”。

程序清單1 定義神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)

 1  |  import numpy as np
 2  |  import tensorflow as tf
 3  |  
 4  |  class ANN(object):
 5  |      # 省略掉其他部分
 6  |  
 7  |      def defineANN(self):
 8  |          """
 9  |          定義神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)
10  |          """
11  |          # self.input是訓(xùn)練數(shù)據(jù)里自變量
12  |          prevSize = self.input.shape[1].value
13  |          prevOut = self.input
14  |          # self.size是神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu),也就是每一層的神經(jīng)元個數(shù)
15  |          size = self.size
16  |          # 定義隱藏層
17  |          for currentSize in size[:-1]:
18  |              weights = tf.Variable(
19  |                  tf.truncated_normal([prevSize, currentSize],
20  |                      stddev=1.0 / np.sqrt(float(prevSize))))
21  |              biases = tf.Variable(tf.zeros([currentSize]))
22  |              prevOut = tf.nn.sigmoid(tf.matmul(prevOut, weights) + biases)
23  |              prevSize = currentSize
24  |          # 定義輸出層
25  |          weights = tf.Variable(
26  |              tf.truncated_normal([prevSize, size[-1]],
27  |                  stddev=1.0 / np.sqrt(float(prevSize))))
28  |          biases = tf.Variable(tf.zeros([size[-1]]))
29  |          self.out = tf.matmul(prevOut, weights) + biases
30  |          return self

第二步是定義神經(jīng)網(wǎng)絡(luò)的損失函數(shù),如程序清單2所示。

  1. 在ANN類中,“self.label”對應(yīng)著訓(xùn)練數(shù)據(jù)里的標(biāo)簽變量(它的類型是tf.placeholder)。值得注意的是,這里用到的標(biāo)簽變量是使用One-Hot Encoding(獨(dú)熱編碼)處理過的。比如針對圖1中的數(shù)據(jù),每個數(shù)據(jù)的標(biāo)簽變量是二維的行向量,用表示類別0,用表示類別1。
  2. 在ANN類中,“self.out”對應(yīng)著神經(jīng)網(wǎng)絡(luò)的輸出層,具體的定義如程序清單2中的第29行代碼所示。
  3. 根據(jù)《神經(jīng)網(wǎng)絡(luò)(一)》、《神經(jīng)網(wǎng)絡(luò)(二)》和《神經(jīng)網(wǎng)絡(luò)(三)》中的討論結(jié)果,神經(jīng)網(wǎng)絡(luò)的單點(diǎn)損失的實(shí)現(xiàn)如第9、10行代碼所示,其中,“self.out”對應(yīng)著公式里的變量。
  4. 模型的整體損失等于所有單點(diǎn)損失之和,相應(yīng)的實(shí)現(xiàn)如第12行代碼所示。

程序清單2 定義神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)

 1  |  class ANN(object):
 2  |      # 省略掉其他部分
 3  |  
 4  |      def defineLoss(self):
 5  |          """
 6  |          定義神經(jīng)網(wǎng)絡(luò)的損失函數(shù)
 7  |          """
 8  |          # 定義單點(diǎn)損失,self.label是訓(xùn)練數(shù)據(jù)里的標(biāo)簽變量
 9  |          loss = tf.nn.softmax_cross_entropy_with_logits(
10  |              labels=self.label, logits=self.out, name="loss")
11  |          # 定義整體損失
12  |          self.loss = tf.reduce_mean(loss, name="average_loss")
13  |          return self

第三步是訓(xùn)練神經(jīng)網(wǎng)絡(luò),如程序清單3所示。

  1. 從理論上來講,訓(xùn)練神經(jīng)網(wǎng)絡(luò)的算法是之后將討論的反向傳播算法,這個算法的基礎(chǔ)是隨機(jī)梯度下降法(stochastic gradient descent)。由于TensorFlow已經(jīng)將整個算法包裝好了,如第8~23行代碼所示。限于篇幅,實(shí)現(xiàn)的具體細(xì)節(jié)在此就不再重復(fù)了。

  2. 如果將訓(xùn)練過程的模型損失(隨訓(xùn)練輪次的變化曲線)記錄下來,可以得到如圖4所示的圖像,其中曲線的標(biāo)記對應(yīng)著訓(xùn)練數(shù)據(jù)的標(biāo)記。從圖中的結(jié)果可以看到,對于不同類型的數(shù)據(jù),模型損失函數(shù)的變化曲線是不一樣的。對于比較難訓(xùn)練的數(shù)據(jù)(標(biāo)記3),模型的損失經(jīng)歷了一個很漫長的訓(xùn)練瓶頸期。也就是說,雖然模型并沒有達(dá)到收斂狀態(tài),但在較長的訓(xùn)練周期里,模型效果幾乎沒有提升。這種現(xiàn)象其實(shí)是神經(jīng)網(wǎng)絡(luò)研究領(lǐng)域里最大的難點(diǎn),它使得神經(jīng)網(wǎng)絡(luò)的訓(xùn)練(特別是層數(shù)較多深度神經(jīng)網(wǎng)絡(luò))變得極其困難,一方面瓶頸期會使模型的訓(xùn)練變得非常漫長;另一方面,在實(shí)際應(yīng)用中,當(dāng)模型損失不再大幅變動時,我們很難判斷這是因?yàn)槟P偷竭_(dá)了收斂狀態(tài)還是因?yàn)槟P瓦M(jìn)入了瓶頸期[2]。引起瓶頸期這種現(xiàn)象的原因有很多,我們將在后面的文章中重點(diǎn)討論這部分內(nèi)容。

圖4

程序清單3 訓(xùn)練模型

1  |  class ANN(object):
2  |      # 省略掉其他部分
3  |  
4  |      def SGD(self, X, Y, learningRate, miniBatchFraction, epoch):
5  |          """
6  |          使用隨機(jī)梯度下降法訓(xùn)練模型
7  |          """
8  |          method = tf.train.GradientDescentOptimizer(learningRate)
9  |          optimizer= method.minimize(self.loss)
10  |          batchSize = int(X.shape[0] * miniBatchFraction)
11  |          batchNum = int(np.ceil(1 / miniBatchFraction))
12  |          sess = tf.Session()
13  |          init = tf.global_variables_initializer()
14  |          sess.run(init)
15  |          step = 0
16  |          while (step < epoch):
17  |              for i in range(batchNum):
18  |                  batchX = X[i * batchSize: (i + 1) * batchSize]
19  |                  batchY = Y[i * batchSize: (i + 1) * batchSize]
20  |                  sess.run([optimizer],
21  |                      feed_dict={self.input: batchX, self.label: batchY})
22  |              step += 1
23  |          self.sess = sess
24  |          return self

神經(jīng)網(wǎng)絡(luò)訓(xùn)練好之后,就可以使用它對未知數(shù)據(jù)做預(yù)測,如程序清單4所示。根據(jù)前面的討論,對神經(jīng)網(wǎng)絡(luò)的輸出層使用softmax函數(shù),就可以得到每個類別的預(yù)測概率,具體的實(shí)現(xiàn)如第9、10行代碼所示。

程序清單4 對未知數(shù)據(jù)做預(yù)測

 1  |  class ANN(object):
 2  |      # 省略掉其他部分
 3  |  
 4  |      def predict_proba(self, X):
 5  |          """
 6  |          使用神經(jīng)網(wǎng)絡(luò)對未知數(shù)據(jù)進(jìn)行預(yù)測
 7  |          """
 8  |          sess = self.sess
 9  |          pred = tf.nn.softmax(logits=self.out, name="pred")
10  |          prob = sess.run(pred, feed_dict={self.input: X})
11  |          return prob

四、廣告時間

這篇文章的大部分內(nèi)容參考自我的新書《精通數(shù)據(jù)科學(xué):從線性回歸到深度學(xué)習(xí)》

李國杰院士和韓家煒教授在讀過此書后,親自為其作序,歡迎大家購買。

另外,與之相關(guān)的免費(fèi)視頻課程請關(guān)注這個鏈接


  1. 例子參考自GitHub上的開源項(xiàng)目tensorflow/playground。完整的實(shí)現(xiàn)請請參考隨書配套的代碼/ch12-ann/ classification_example.py ?

  2. 雖然對于特定的應(yīng)用場景,我們在數(shù)學(xué)上可以找到一些判斷瓶頸期的依據(jù),但從整體上來說并沒有特別通用的辦法,這一點(diǎn)也顯示了人類對神經(jīng)網(wǎng)絡(luò)的理解是十分薄弱的 ?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容