?深度學(xué)習(xí)敲門磚-系列筆記
從【DL筆記1】到【DL筆記N】,是我學(xué)習(xí)深度學(xué)習(xí)一路上的點(diǎn)點(diǎn)滴滴的記錄,是從Coursera網(wǎng)課、各大博客、論文的學(xué)習(xí)以及自己的實(shí)踐中總結(jié)而來。從基本的概念、原理、公式,到用生動(dòng)形象的例子去理解,到動(dòng)手做實(shí)驗(yàn)去感知,到著名案例的學(xué)習(xí),到用所學(xué)來實(shí)現(xiàn)自己的小而有趣的想法......我相信,一路看下來,我們可以感受到深度學(xué)習(xí)的無窮的樂趣,并有興趣和激情繼續(xù)鉆研學(xué)習(xí)。
正所謂 Learning by teaching,寫下一篇篇筆記的同時(shí),我也收獲了更多深刻的體會(huì),希望大家可以和我一同進(jìn)步,共同享受AI無窮的樂趣。
本篇文章我們會(huì)使用兩種框架(TensorFlow和Keras,雖然Keras從某種意義上是TF的一種高層API)來實(shí)現(xiàn)一個(gè)簡單的CNN,來對我們之前的MNIST手寫數(shù)字進(jìn)行識別。還記得上一次我們用TF實(shí)現(xiàn)了一個(gè)簡單的三層神經(jīng)網(wǎng)絡(luò),最終測試集準(zhǔn)確率達(dá)到95%的水平。今天,我們期望達(dá)到99%以上的準(zhǔn)確率!
在開始前,我們先確定一下我們的網(wǎng)絡(luò)結(jié)構(gòu):
我們也不用弄很復(fù)雜的,畢竟這個(gè)手寫數(shù)字識別實(shí)際上很容易了,所以我們設(shè)計(jì)兩層卷積(后接池化層),然后一個(gè)全連接層,最后用Softmax輸出即可。
這里需要說的一點(diǎn)是:
卷積層、池化層,處理的輸入都是三維的(長、寬、通道),但是全連接層處理的輸入?yún)s是一維的,因此我們需要在FC層之前對輸入數(shù)據(jù)進(jìn)行“壓扁”處理,把每一維的每一個(gè)單元全部排列成一條直線!
在下面的代碼中,我們可以看到在不同的框架中是怎么操作的。
import tensorflow as tfsess = tf.InteractiveSession()
import numpy as np
from tensorflow.examples.tutorials.mnist import input_datamnist = input_data.read_data_sets('MNIST_data/', one_hot=True)X_train,Y_train = mnist.train.images,mnist.train.labelsX_test,Y_test = mnist.test.images,mnist.test.labelsprint(X_train.shape)print(Y_train.shape)print(X_test.shape)print(Y_test.shape)
## 輸出,看看數(shù)據(jù)的形狀:
Extracting MNIST_data/train-images-idx3-ubyte.gzExtracting MNIST_data/train-labels-idx1-ubyte.gzExtracting MNIST_data/t10k-images-idx3-ubyte.gzExtracting MNIST_data/t10k-labels-idx1-ubyte.gz(55000, 784)(55000, 10)(10000, 784)(10000, 10)
這里需要多說一句的就是這個(gè)InteractiveSession。
還記得我們上次使用TF的時(shí)候,是用sess=tf.Session()
或者with tf.Session() as sess:
的方法來啟動(dòng)session。
他們兩者有什么區(qū)別呢:InteractiveSession()
,多用于交互式的環(huán)境中,如IPython Notebooks,比Session()
更加方便靈活。
在前面我們知道,我們所有的計(jì)算,都必須在:
with tf.Session as sess: ... ...
中進(jìn)行,如果出界了,就會(huì)報(bào)錯(cuò),說“當(dāng)前session中沒有這個(gè)操作/張量”這種話。
但是在InteractiveSession()
中,我們只要?jiǎng)?chuàng)建了,就會(huì)全局默認(rèn)是這個(gè)session,我們可以隨時(shí)添加各種操作,用Tensor.eval()
和Operation.run()
來隨機(jī)進(jìn)行求值和計(jì)算。
為了方便,我們不是直接給變量賦值,而是寫一些通用的函數(shù):
對于weights,我們在前面的文章【】中提到過,需要隨機(jī)初始化參數(shù),不能直接用0來初始化,否則可能導(dǎo)致無法訓(xùn)練。因此我們這里采用truncated_normal方法,normal分布就是我們熟悉的正態(tài)分布,truncated_normal就是將正太分布的兩端的過大過小的值去掉了,使得我們的訓(xùn)練更容易。
而對于bias,它怎么初始化就無所謂了,我們簡單起見,就直接用0初始化了。
def weights(shape): initial = tf.truncated_normal(shape,stddev=0.1) return tf.Variable(initial)
def bias(shape): initial = tf.zeros(shape) return tf.Variable(initial)
然后一些定義層的函數(shù):
定義卷積層、池化層的時(shí)候有很多超參數(shù)要設(shè)置,但是很多參數(shù)可能是一樣的,所以我們寫一個(gè)函數(shù)會(huì)比較方便。
參數(shù)的格式有必要在這里說一說,因?yàn)檫@個(gè)很容易搞混淆:
對于卷積層:
輸入(inputs/X):[batch, height, width, channels],就是[樣本量,高,寬,通道數(shù)];
權(quán)重(filter/W):[filter_height, filter_width, in_channels, out_channels],這個(gè)就不解釋了吧,英語都懂哈;
步長(strides):一開始我很奇怪為什么會(huì)是四維的數(shù)組,查看文檔才只有,原來是對應(yīng)于input每一維的步長。而我們一般只關(guān)注對寬、高的步長,所以假如我們希望步長是2,那么stride就是[1,2,2,1];
填白(padding):這個(gè)是我們之前講過的,如果不填白,就設(shè)為VALID,如果要填白使得卷積后大小不變,那么就用SAME.
對于池化層:
輸入、步長跟上面一樣;
ksize:指的是kernel-size,就是在pooling的時(shí)候的窗口,也是一個(gè)四維數(shù)組,對應(yīng)于輸入的每一維。跟strides一樣,我們一般只關(guān)心中間兩個(gè)維度,比如我們希望一個(gè)2×2的窗口,就設(shè)為[1,2,2,1].
(這里有一個(gè)經(jīng)驗(yàn):當(dāng)stride為2,窗口也為2的時(shí)候,這個(gè)Maxpool實(shí)際上把前面的輸入圖像的長寬各縮小了一半!)
這里的padding和卷積層的padding有所不同!這里的padding是指,當(dāng)窗口的大小和步長設(shè)置不能正好覆蓋原圖時(shí),需不需要填白使得可以正好覆蓋。一般我們都選SAME,表示要填白。
def conv(X,W): return tf.nn.conv2d(X,W,strides=[1,1,1,1],padding='SAME') def max_pool(X): return tf.nn.max_pool(X,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
好了,準(zhǔn)備工作都做好了,我們可以開始正式搭建網(wǎng)絡(luò)結(jié)構(gòu)了!
先為X,Y定義一個(gè)占位符,后面運(yùn)行的時(shí)候再注入數(shù)據(jù):
X= tf.placeholder(dtype=tf.float32,shape=[None,784])
## 由于我們的卷積層處理的是四維的輸入,所以我們這里需要對數(shù)據(jù)進(jìn)行變形:
X_image = tf.reshape(X,[-1,28,28,1]) # -1代表這一維度可以變化
Y = tf.placeholder(dtype=tf.float32,shape=[None,10])
我們就來兩個(gè)卷積層(filter為5×5,每層后面都接一個(gè)池化層),后接一個(gè)全連接層吧:
## 第一個(gè)卷積層:
W1 = weights([5,5,1,32])b1 = bias([32])A1 = tf.nn.relu(conv(X_image,W1)+b1,name='relu1')A1_pool = max_pool(A1)
## 第二個(gè)卷積層:
W2 = weights([5,5,32,64])b2 = bias([64])A2 = tf.nn.relu(conv(A1_pool,W2)+b2,name='relu2')A2_pool = max_pool(A2)
## 全連接層:
## 前面經(jīng)過兩個(gè)CONV,將圖片的通道數(shù)變成了64個(gè),
## 另外經(jīng)過兩個(gè)POOL,將原來28×28的圖片,縮小成了7×7,
## 因此,壓扁后,會(huì)有7*7*64個(gè)單元:
W3 = weights([7*7*64,128])b3 = bias([128])
## 對數(shù)據(jù)進(jìn)行壓扁處理:
A2_flatten = tf.reshape(A2_pool,[-1,7*7*64])A3= tf.nn.relu(tf.matmul(A2_flatten,W3)+b3,name='relu3')
## 輸出層(Softmax):
W4 = weights([128,10])b4 = bias([10])Y_pred = tf.nn.softmax(tf.matmul(A3,W4)+b4)
定義損失和優(yōu)化器:
loss = -tf.reduce_sum(Y*tf.log(Y_pred))train_step = tf.train.AdamOptimizer(0.001).minimize(loss)
## 先要初始化所有的變量:
sess.run(tf.global_variables_initializer())
## 然后訓(xùn)練它個(gè)幾千次:(用CPU計(jì)算,用時(shí)大概20分鐘)
costs = []
for i in range(3000): X_batch,Y_batch = mnist.train.next_batch(batch_size=64) _,batch_cost = sess.run([train_step,loss],feed_dict={X:X_batch,Y:Y_batch}) if i%100 == 0: print('Batch%d cost:'%i,batch_cost) costs.append(batch_cost)print('Training finished!')
## 部分輸出:
Batch0 cost: 0.16677594Batch100 cost: 0.052068923Batch200 cost: 0.5979577Batch300 cost: 0.049106397Batch400 cost: 0.047060404Batch500 cost: 2.0360851Batch600 cost: 3.3168547Batch700 cost: 0.11393449Batch800 cost: 0.06208247Batch900 cost: 0.035165284Training finished!
## 計(jì)算測試集準(zhǔn)確率:
correct_prediction = tf.equal(tf.argmax(Y_pred,1),tf.argmax(Y,1))accuracy = tf.reduce_mean(tf.cast(correct_prediction,'float'))accuracy.eval(feed_dict={X:X_test,Y:Y_test})
## 得到測試集準(zhǔn)確率:
0.9927
經(jīng)過幾千次的迭代,準(zhǔn)確率已經(jīng)達(dá)到了99%以上!CNN果真效果不錯(cuò)!
上面,我們已經(jīng)用TensorFlow搭建了一個(gè)4層的卷積神經(jīng)網(wǎng)絡(luò)(2個(gè)卷積層,兩個(gè)全連接層,注意,最后的Softmax層實(shí)際上也是一個(gè)全連接層)
接下來,我們用Keras來搭建一個(gè)一模一樣的模型,來對比一下二者在實(shí)現(xiàn)上的差異。
import keras
from keras.models import Sequential
from keras.layers import Conv2D,MaxPooling2D,Dense,Flatten
下面搭建網(wǎng)絡(luò)結(jié)構(gòu):
model = Sequential()
# 第一個(gè)卷積層(后接池化層):
model.add(Conv2D(32,kernel_size=(5,5),padding='same',activation='relu',input_shape=(28,28,1)))model.add(MaxPooling2D(pool_size=(2,2),padding='same'))
# 第二個(gè)卷積層(后接池化層):
model.add(Conv2D(64,kernel_size=(5,5),padding='same',activation='relu'))model.add(MaxPooling2D(pool_size=(2,2),padding='same'))
# 將上面的結(jié)果扁平化,然后接全連接層:
model.add(Flatten())model.add(Dense(128,activation='relu'))
#最后一個(gè)Softmax輸出:
model.add(Dense(10,activation='softmax'))
編譯模型,設(shè)定優(yōu)化器和損失:
model.compile(optimizer='Adam',loss='categorical_crossentropy',metrics=['accuracy'])
開始訓(xùn)練:
這里我們先將輸入數(shù)據(jù)變形,跟前面TensorFlow中的一樣:
X_train_image = X_train.reshape(X_train.shape[0],28,28,1)X_test_image = X_test.reshape(X_test.shape[0],28,28,1)
# 開始訓(xùn)練:
model.fit(X_train_image,Y_train,epochs=6,batch_size=64)
這里的epoch代表迭代的次數(shù),注意,這里為什么我只寫了6次???
前面用TensorFlow的時(shí)候,不是迭代了幾千次嗎?
細(xì)心的讀者會(huì)注意到,用TensorFlow的時(shí)候,我們使用的MNIST數(shù)據(jù)集自帶的一個(gè)取mini-batch的方法,每次迭代只選取55000個(gè)樣本中的64個(gè)來訓(xùn)練,因此雖然迭代了3000多次,但實(shí)際上也就是3000次的更新。
但是在這里,我們傳入的是整個(gè)數(shù)據(jù)集,設(shè)置batch為64,也就是一個(gè)epoch里面,我們會(huì)把原數(shù)據(jù)集分成一個(gè)個(gè)64大小的數(shù)據(jù)包丟進(jìn)去訓(xùn)練,一個(gè)epoch就會(huì)更新55000/64次,因此,雖然epoch=6,但是實(shí)際上參數(shù)也是更新了幾千次。
## 訓(xùn)練過程輸出(部分):
Epoch 1/655000/55000 [==============================] - 138s 3ms/step - loss: 0.0093 - acc: 0.9968Epoch 2/655000/55000 [==============================] - 140s 3ms/step - loss: 0.0064 - acc: 0.9979Epoch 3/655000/55000 [==============================] - 141s 3ms/step - loss: 0.0052 - acc: 0.9982Epoch 4/655000/55000 [==============================] - 141s 3ms/step - loss: 0.0049 - acc: 0.9984Epoch 5/655000/55000 [==============================] - 140s 3ms/step - loss: 0.0060 - acc: 0.9981Epoch 6/655000/55000 [==============================] - 141s 3ms/step - loss: 0.0034 - acc: 0.9991
## 查看測試集準(zhǔn)確率:
result = model.evaluate(X_test_image,Y_test)print('Test accuracy:',result[1])
## 得到結(jié)果:
10000/10000 [==============================] - 10s 969us/stepTest accuracy: 0.9926
測試集準(zhǔn)確率達(dá)到99.26%!與前面TensorFlow的訓(xùn)練結(jié)果基本一致。
對比與總結(jié):
可以看到,在Keras里面搭建網(wǎng)絡(luò)結(jié)構(gòu)是如此的簡單直白,直接往上堆就行了,不用考慮輸入數(shù)據(jù)的維度,而是自動(dòng)進(jìn)行轉(zhuǎn)換。在用TensorFlow的時(shí)候,我們需要手動(dòng)計(jì)算一下,在經(jīng)過每一層后,通道數(shù)、長寬都是變成了多少,并據(jù)此設(shè)置后面的參數(shù),但是在Keras里面,我們只用關(guān)心我的結(jié)構(gòu)到底應(yīng)該怎么設(shè)計(jì),不用關(guān)心數(shù)據(jù)的各維度是怎么變化的,畢竟結(jié)構(gòu)確定了,數(shù)據(jù)的變化是唯一確定的。因此,在這一點(diǎn)上,我是十分喜歡Keras的。
另外,Keras的模型的編譯也十分地簡單,只要清楚相關(guān)的深度學(xué)習(xí)概念,損失函數(shù)都不用我們?nèi)懝?,而是直接選擇,公式都是內(nèi)置的。
我覺得最讓人舒服的一點(diǎn)是,keras里面基本不用考慮session的問題,這就避免了很多我們調(diào)bug的時(shí)間,而這本來不重要。我們搭建好模型就編譯,編譯好模型就訓(xùn)練,一氣呵成,而且keras在訓(xùn)練中,內(nèi)置了日志打印,所以我們很容易看清楚訓(xùn)練的過程怎樣。
上面這么一說,好像Keras什么都好。但其實(shí)這就像是C++和Python的關(guān)系一樣,大家都知道Python好學(xué)容易上手,很多復(fù)雜的東西已經(jīng)封裝成接口我們直接調(diào)用就可以了。但是真正到了復(fù)雜的問題,碰到python沒有封裝好的功能的時(shí)候,我們只能回到底層的C++去尋求答案了。TensorFlow也一樣,雖然有些晦澀難懂,什么session、graph、op搞得人暈頭轉(zhuǎn)向,但是它有許多允許我們DIY的地方,而Keras作為一個(gè)由TensorFlow作為支撐而構(gòu)建的框架,必然會(huì)有很多功能不具備,也會(huì)有諸多限制。
所以如果日常搭建一下模型、復(fù)現(xiàn)一下模型,我覺得keras挺好的,省去了我們很多麻煩。如果有更高級的目標(biāo),例如學(xué)術(shù)上的創(chuàng)新,建議還是耐心地學(xué)一學(xué)TensorFlow。
聯(lián)系客服