1. 程式人生 > >神經網路(四):應用示例之分類

神經網路(四):應用示例之分類

一、 傳統分類模型的侷限

在之前的文章中(《神經網路(一)》《神經網路(二)》《神經網路(三)》),我們討論的重點是神經網路的理論知識。現在來看一個實際的例子,如何利用神經網路解決分類問題。(為了更好地展示神經網路的特點,我們在這個示例中並不劃分訓練集和測試集)。

分類是機器學習最常見的應用之一,之前的章節也討論過很多解決分類問題的機器學習模型,比如邏輯迴歸和支援向量學習機等。但這些模型最大的侷限性是它們都有比較明確的適用範圍,如果訓練資料符合模型的假設,則分類效果很好。否則,分類的效果就會很差。

比如圖11中展示了4種不同分佈型別的資料。具體來說,資料裡有兩個自變數,分別對應著座標系的橫縱軸;資料分為兩類,在圖中用三角形表示類別0,用圓點表示類別1。如果使用邏輯迴歸對資料進行分類,只有圖中標記1中的模型效果較好(圖中的灰色區域裡,模型的預測結果是類別0;白色區域裡,模型的預測結果是類別1),因為在已知類別的情況下,資料服從正態分佈(不同類別,分佈的中心不同),符合邏輯迴歸的模型假設。對於標記2、3、4中的資料,由於類別與自變數之間的關係是非線性的,如果想取得比較好的分類效果,則需要其他的建模技巧。比如先使用核函式對資料進行升維,再使用支援向量學習機進行分類。

圖1

二、 神經網路的優勢

這樣的建模方法是比較辛苦的,要求搭建模型的資料科學家對不同模型的假設以及優缺點有比較深刻的理解。但如果使用神經網路對資料進行分類,則整個建模過程就比較輕鬆了,只需設計神經網路的形狀(包括神經網路的層數以及每一層裡的神經元個數),然後將資料輸入給模型即可。
在這個例子中,使用的神經網路如圖2所示,是一個3-層的全連線神經網路。

圖2

使用這個神經網路對資料進行分類,得到的結果如圖3所示,可以看到同一個神經網路(結構相同,但具體的模型引數是不同的)對4種不同分佈型別的資料都能較好地進行分類。

圖3

這一節節將討論如何藉助第三方庫TensorFlow來實現神經網路,。

第一步是定義神經網路的結構,如程式清單1所示。

  1. 我們使用類(class)來實現神經網路,如第4行程式碼所示。在Python的類中可以定義相應的函式,但在類中,函式的定義與普通函式的定義有所不同,它的引數個數必須大於1,且第一個引數表示類本身,如第7行程式碼裡的“self”變數。但在呼叫這個函式時,卻不需要“手動”地傳入這個引數,Python會自動地進行引數傳遞,比如defineANN函式的呼叫方式是“defineANN()”。
  2. 在ANN類中,“self.input”對應著訓練資料裡的自變數(它的型別是tf.placeholder),如第12行程式碼所示,“self.input.shape[1].value”表示輸入層的神經元個數(針對如圖2的神經網路,這個值等於2)。而“self.size”是表示神經網路結構的陣列(針對如圖2的神經網路,這個值等於[4, 4, 2])。在ANN類中,“self.input”對應著訓練資料裡的自變數(它的型別是tf.placeholder),如第12行程式碼所示,“self.input.shape[1].value”表示輸入層的神經元個數(針對如圖12-8的神經網路,這個值等於2)。而“self.size”是表示神經網路結構的陣列(針對如圖2的神經網路,這個值等於[4, 4, 2])。
  3. 接下來是定義網路的隱藏層。首先是神經元裡的線性模型部分,如第18~21行程式碼所示,定義權重項“weights”和截距項“biases”。因此,權重項是一個的矩陣,而截距項是一個維度等於的行向量。值得注意的是,在定義權重項時,使用tf.truncated_normal函式(近似地對應著正態分佈)來生成初始值,在生成初始值的過程中,我們用如下的命令來規定分佈的標準差“stddev=1.0 / np.sqrt(float(prevSize))”,這樣操作的原因是為了使神經網路更快收斂。定義好線性模型後,就需要定義神經元的啟用函式,如第22行程式碼所示,使用的啟用函式是tf.nn.sigmoid,它對應著sigmoid函式。
  4. 最後是定義神經網路的輸出層,如第25~29行程式碼所示。具體的過程和隱藏層類似,唯一不同的是,輸出層並沒有啟用函式,因此只需定義線性模型部分“tf.matmul(prevOut, weights) + biases”。

程式清單1 定義神經網路的結構

 1	|  import numpy as np
 2	|  import tensorflow as tf
 3	|  
 4	|  class ANN(object):
 5	|      # 省略掉其他部分
 6	|  
 7	|      def defineANN(self):
 8	|          """
 9	|          定義神經網路的結構
10	|          """
11	|          # self.input是訓練資料裡自變數
12	|          prevSize = self.input.shape[1].value
13	|          prevOut = self.input
14	|          # self.size是神經網路的結構,也就是每一層的神經元個數
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

第二步是定義神經網路的損失函式,如程式清單2所示。

  1. 在ANN類中,“self.label”對應著訓練資料裡的標籤變數(它的型別是tf.placeholder)。值得注意的是,這裡用到的標籤變數是使用One-Hot Encoding(獨熱編碼)處理過的。比如針對圖1中的資料,每個資料的標籤變數是二維的行向量,用表示類別0,用表示類別1。
  2. 在ANN類中,“self.out”對應著神經網路的輸出層,具體的定義如程式清單2中的第29行程式碼所示。
  3. 根據《神經網路(一)》、《神經網路(二)》和《神經網路(三)》中的討論結果,神經網路的單點損失的實現如第9、10行程式碼所示,其中,“self.out”對應著公式裡的變數。
  4. 模型的整體損失等於所有單點損失之和,相應的實現如第12行程式碼所示。

程式清單2 定義神經網路的結構

 1	|  class ANN(object):
 2	|      # 省略掉其他部分
 3	|  
 4	|      def defineLoss(self):
 5	|          """
 6	|          定義神經網路的損失函式
 7	|          """
 8	|          # 定義單點損失,self.label是訓練資料裡的標籤變數
 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

第三步是訓練神經網路,如程式清單3所示。

  1. 從理論上來講,訓練神經網路的演算法是之後將討論的反向傳播演算法,這個演算法的基礎是隨機梯度下降法(stochastic gradient descent)。由於TensorFlow已經將整個演算法包裝好了,如第8~23行程式碼所示。限於篇幅,實現的具體細節在此就不再重複了。

  2. 如果將訓練過程的模型損失(隨訓練輪次的變化曲線)記錄下來,可以得到如圖4所示的影象,其中曲線的標記對應著訓練資料的標記。從圖中的結果可以看到,對於不同型別的資料,模型損失函式的變化曲線是不一樣的。對於比較難訓練的資料(標記3),模型的損失經歷了一個很漫長的訓練瓶頸期。也就是說,雖然模型並沒有達到收斂狀態,但在較長的訓練週期裡,模型效果幾乎沒有提升。這種現象其實是神經網路研究領域裡最大的難點,它使得神經網路的訓練(特別是層數較多深度神經網路)變得極其困難,一方面瓶頸期會使模型的訓練變得非常漫長;另一方面,在實際應用中,當模型損失不再大幅變動時,我們很難判斷這是因為模型到達了收斂狀態還是因為模型進入了瓶頸期2。引起瓶頸期這種現象的原因有很多,我們將在後面的文章中重點討論這部分內容。

圖4

程式清單3 訓練模型

1	|  class ANN(object):
2	|      # 省略掉其他部分
3	|  
4	|      def SGD(self, X, Y, learningRate, miniBatchFraction, epoch):
5	|          """
6	|          使用隨機梯度下降法訓練模型
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

神經網路訓練好之後,就可以使用它對未知資料做預測,如程式清單4所示。根據前面的討論,對神經網路的輸出層使用softmax函式,就可以得到每個類別的預測概率,具體的實現如第9、10行程式碼所示。

程式清單4 對未知資料做預測

 1	|  class ANN(object):
 2	|      # 省略掉其他部分
 3	|  
 4	|      def predict_proba(self, X):
 5	|          """
 6	|          使用神經網路對未知資料進行預測
 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

四、廣告時間

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

另外,與之相關的免費視訊課程請關注這個連結

  1. 例子參考自GitHub上的開源專案tensorflow/playground。完整的實現請請參考隨書配套的程式碼/ch12-ann/ classification_example.py ↩︎

  2. 雖然對於特定的應用場景,我們在數學上可以找到一些判斷瓶頸期的依據,但從整體上來說並沒有特別通用的辦法,這一點也顯示了人類對神經網路的理解是十分薄弱的 ↩︎