一、 傳統(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)行分類。

二、 神經(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ò)。

使用這個神經(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)行分類。

三、 代碼實(shí)現(xiàn)(完整的代碼請見)
這一節(jié)節(jié)將討論如何借助第三方庫TensorFlow來實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò),。
第一步是定義神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu),如程序清單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()”。
- 在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])。
- 接下來是定義網(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ù)。
- 最后是定義神經(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所示。
- 在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。
- 在ANN類中,“self.out”對應(yīng)著神經(jīng)網(wǎng)絡(luò)的輸出層,具體的定義如程序清單2中的第29行代碼所示。
- 根據(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)著公式里的變量。
- 模型的整體損失等于所有單點(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所示。
從理論上來講,訓(xùn)練神經(jīng)網(wǎng)絡(luò)的算法是之后將討論的反向傳播算法,這個算法的基礎(chǔ)是隨機(jī)梯度下降法(stochastic gradient descent)。由于TensorFlow已經(jīng)將整個算法包裝好了,如第8~23行代碼所示。限于篇幅,實(shí)現(xiàn)的具體細(xì)節(jié)在此就不再重復(fù)了。
如果將訓(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)容。

程序清單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)注這個鏈接