一膏蚓、 傳統(tǒng)分類模型的局限
在之前的文章中(《神經(jīng)網(wǎng)絡(luò)(一)》瓢谢、《神經(jīng)網(wǎng)絡(luò)(二)》和《神經(jīng)網(wǎng)絡(luò)(三)》),我們討論的重點是神經(jīng)網(wǎng)絡(luò)的理論知識⊥郧疲現(xiàn)在來看一個實際的例子,如何利用神經(jīng)網(wǎng)絡(luò)解決分類問題论笔。(為了更好地展示神經(jīng)網(wǎng)絡(luò)的特點翅楼,我們在這個示例中并不劃分訓(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肌毅,用圓點表示類別1。如果使用邏輯回歸對數(shù)據(jù)進(jìn)行分類姑原,只有圖中標(biāo)記1中的模型效果較好(圖中的灰色區(qū)域里悬而,模型的預(yù)測結(jié)果是類別0;白色區(qū)域里锭汛,模型的預(yù)測結(jié)果是類別1)摊滔,因為在已知類別的情況下,數(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)缺點有比較深刻的理解没陡。但如果使用神經(jīng)網(wǎng)絡(luò)對數(shù)據(jù)進(jìn)行分類,則整個建模過程就比較輕松了索赏,只需設(shè)計神經(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)行分類。
三剃斧、 代碼實現(xiàn)(完整的代碼請見)
這一節(jié)節(jié)將討論如何借助第三方庫TensorFlow來實現(xiàn)神經(jīng)網(wǎng)絡(luò)杂拨,。
第一步是定義神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)悯衬,如程序清單1所示弹沽。
- 我們使用類(class)來實現(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)重項“weights”和截距項“biases”。因此插勤,權(quán)重項是一個的矩陣沽瘦,而截距項是一個維度等于的行向量革骨。值得注意的是,在定義權(quán)重項時析恋,使用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(獨熱編碼)處理過的谒麦。比如針對圖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ò)的單點損失的實現(xiàn)如第9、10行代碼所示漱牵,其中夺蛇,“self.out”對應(yīng)著公式里的變量。
- 模型的整體損失等于所有單點損失之和酣胀,相應(yīng)的實現(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 | # 定義單點損失娶聘,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行代碼所示。限于篇幅猴凹,實現(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)象其實是神經(jīng)網(wǎng)絡(luò)研究領(lǐng)域里最大的難點,它使得神經(jīng)網(wǎng)絡(luò)的訓(xùn)練(特別是層數(shù)較多深度神經(jīng)網(wǎng)絡(luò))變得極其困難烙如,一方面瓶頸期會使模型的訓(xùn)練變得非常漫長么抗;另一方面,在實際應(yīng)用中亚铁,當(dāng)模型損失不再大幅變動時蝇刀,我們很難判斷這是因為模型到達(dá)了收斂狀態(tài)還是因為模型進(jìn)入了瓶頸期[2]。引起瓶頸期這種現(xià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ù)測概率奴烙,具體的實現(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)的免費視頻課程請關(guān)注這個鏈接