本文介紹如何基于Tensorflow的Slim庫(kù),利用CNN(卷積神經(jīng)網(wǎng)絡(luò))實(shí)現(xiàn)手寫(xiě)數(shù)字識(shí)別。
首先介紹一些基本概念:
- tensorflow庫(kù)
placeholder和variable的區(qū)別:
placeholder是占位符,用于定義模型的輸入和輸出,定義的時(shí)候不賦值,使用模型的時(shí)候才賦值。variable是變量,定義的時(shí)候就需要賦值,而且隨著模型的訓(xùn)練過(guò)程不停的優(yōu)化,比如權(quán)重和偏差。tf.Variable和tf.get_variable
tf.Variable會(huì)新建一個(gè)變量,變量名相同的情況下通過(guò)添加后綴編號(hào)識(shí)別不同的變量;tf.get_variable如果發(fā)現(xiàn)有同名變量就復(fù)用,否則新建變量。tf.nn.softmax
softmax就是將一個(gè)數(shù)組中的所有值轉(zhuǎn)換成概率,每個(gè)值的概率和自己的大小成正比,所有數(shù)的概率和為1。-
tf.name_scope和tf.variable_scope的區(qū)別:
它們總的作用都是在變量名前面添加一個(gè)scope前綴,通過(guò)這種方式來(lái)給不同的變量分組。如:
a_name_scope/var2:0
a_name_scope/var2_1:0
a_name_scope/var2_2:0a_variable_scope/var3:0
a_variable_scope/var3_1:0區(qū)別在于tf.name_scope只對(duì)tf.Variable(name='var2', ...)建立的變量有效,對(duì)tf.get_variable(name='var1')這種方式建立的變量無(wú)效;而tf.variable_scope對(duì)兩者都有效。tf.variable_scope更常用一些。
- slim庫(kù)
tf.contrib.slim庫(kù)是一個(gè)基于tensorflow的機(jī)器學(xué)習(xí)庫(kù)(除了slim庫(kù),還有tf.contrib.learn、tf.contrib.keras等其他封裝了tensorflow的庫(kù),可以隨意組合使用)。slim庫(kù)提供了一些常用模型的實(shí)現(xiàn),封裝了模型底層的細(xì)節(jié),使得開(kāi)發(fā)者用起來(lái)更加簡(jiǎn)潔,代碼可靠性和可讀性都大大增強(qiáng)。 - numpy庫(kù)
一個(gè)數(shù)學(xué)庫(kù),集成了一些對(duì)數(shù)組和矩陣的操作。 - python with關(guān)鍵字的使用
with封裝了對(duì)某個(gè)對(duì)象的初始化工作和清理工作,類(lèi)似于自動(dòng)執(zhí)行構(gòu)造函數(shù)和析構(gòu)函數(shù),并且能夠自動(dòng)處理獲取資源(比如打開(kāi)/關(guān)閉文件)過(guò)程中的異常,適合于文件讀取、資源獲取等場(chǎng)景。 - Batch Normalization
Batch Normalization是指對(duì)一個(gè)batch的數(shù)據(jù)通過(guò)計(jì)算均值和方差,并以均值和方差為基礎(chǔ),進(jìn)行一系列的計(jì)算,使得batch里面所有數(shù)據(jù)歸一化到某個(gè)范圍的處理方式。這種歸一化處理可以避免數(shù)據(jù)兩極化分布,提高接下來(lái)的激活函數(shù)處理的有效性。同時(shí)注意,只有批量處理打到一定的數(shù)目,Batch Normalization才有作用,如果一次訓(xùn)練只使用一個(gè)或很少數(shù)的樣本,則無(wú)效。
接下來(lái)通過(guò)代碼講解如何實(shí)現(xiàn)訓(xùn)練和預(yù)測(cè)過(guò)程。
第一步:定義網(wǎng)絡(luò):
def CNN(inputs, is_training=True):
# 將1*784的輸入數(shù)據(jù)reshape成28*28的ndArray
shaped_inputs = tf.reshape(inputs, [-1, height, width, 1]) # NHWC N:Sample的數(shù)量 HW:高和寬 C=1 一個(gè)通道,灰度值
batch_norm_params = {'is_training': is_training, 'decay': 0.9, 'updates_collections': None}
init_func = tf.truncated_normal_initializer(stddev=0.01) # 正太分布初始化
with slim.arg_scope([slim.conv2d],
padding='SAME',
activation_fn=lrelu,
weights_initializer=init_func,
normalizer_fn=slim.batch_norm,
normalizer_params=batch_norm_params):
# 第一個(gè)卷積層 16個(gè)卷積核
net = slim.conv2d(shaped_inputs, 16, [5, 5], scope='conv0')
# 第一個(gè)池化層
net = slim.max_pool2d(net, [2, 2], scope='pool0')
# 第二個(gè)卷積層 32個(gè)卷積核
net = slim.conv2d(net, 32, [5, 5], scope='conv1')
# 第二個(gè)池化層
net = slim.max_pool2d(net, [2, 2], scope='pool1')
# 第三個(gè)卷積層 64個(gè)卷積核
net = slim.conv2d(net, 64, [5, 5], scope='conv2')
# 第三個(gè)池化層
net = slim.max_pool2d(net, [2, 2], scope='pool2')
# 把矩陣flattern成一維的,[batch_size, k]
net = slim.flatten(net, scope='flatten3')
# 第一個(gè)全連接層
net = slim.fully_connected(net, 1024,
activation_fn=lrelu,
weights_initializer=init_func,
normalizer_fn=slim.batch_norm,
normalizer_params=batch_norm_params,
scope='fc4')
net = slim.dropout(net, keep_prob=0.7, is_training=is_training, scope='dr')
# 第二個(gè)全連接層,輸出為10個(gè)類(lèi)別
out = slim.fully_connected(net, n_classes, activation_fn=None, normalizer_fn=None, scope='fco')
return out
這里我們定義的神經(jīng)網(wǎng)絡(luò)包括三個(gè)卷積層和兩個(gè)全連接層,每個(gè)卷積層緊跟一個(gè)池化層。輸入值的維度是batch_sizex784,代表batch_size個(gè)圖片,每個(gè)圖片大小是28x28,轉(zhuǎn)換成一維的數(shù)據(jù)就是784。輸出層維度是batch_sizex10,每一項(xiàng)是一個(gè)1x10個(gè)數(shù)組,代表一張圖片屬于0-9每個(gè)數(shù)字的概率,最大的概率對(duì)應(yīng)的數(shù)字就是分類(lèi)的結(jié)果數(shù)字。
三個(gè)卷積層共享同樣的padding、激活函數(shù)、Batch Normalization方法,所以我們提出來(lái)放到arg_scope里面。
第一個(gè)卷積層有16個(gè)卷積核,代表一張?jiān)驾斎雸D片經(jīng)過(guò)該卷積層會(huì)生成16張新的圖片。第二層32個(gè)卷積核,代表在這一層每張輸入的圖片會(huì)生成32張新的圖片,依次類(lèi)推,第三個(gè)卷積層輸出的圖片總是是16x32x64=2^15。接下來(lái)是兩個(gè)全連接層,為了將卷積層的輸出接入到全連接層,需要通過(guò)slim.flatten函數(shù)將輸出數(shù)據(jù)轉(zhuǎn)換成[batch_size, 2^15] 維的數(shù)據(jù)。第一個(gè)全連接層共1024個(gè)節(jié)點(diǎn),將2^15個(gè)數(shù)據(jù)映射到1024個(gè)節(jié)點(diǎn);第二個(gè)全連接層將這1024個(gè)節(jié)點(diǎn)映射到最終10個(gè)節(jié)點(diǎn)上,代表10個(gè)類(lèi)別的結(jié)果。
第二步:定義輸入輸出,Loss和Optimizer
# 定義模型的輸入輸出
x = tf.placeholder("float", shape=(None, 28 * 28), name="w1") # 輸入的圖像28*28
y = tf.placeholder("float", shape=(None, n_classes), name="w2") # 輸出的標(biāo)簽 1*10
is_training = tf.placeholder(tf.bool, name="w3") # 標(biāo)志位,是訓(xùn)練還是預(yù)測(cè)
# 網(wǎng)絡(luò)計(jì)算
pred = CNN(x, is_training)
# 預(yù)測(cè)的時(shí)候使用這個(gè)節(jié)點(diǎn)的值,選10個(gè)分類(lèi)中概率最大的一個(gè)作為預(yù)測(cè)結(jié)果
out_result = tf.arg_max(pred, 1, name="op_to_restore")
# 定義LOSS和OPTIMIZER
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y, logits=pred)) # 計(jì)算輸出和標(biāo)記結(jié)果的交叉熵作為損失函數(shù)
optm = tf.train.AdamOptimizer(learning_rate=0.001).minimize(cost)
# 定義準(zhǔn)確率
corr = tf.equal(tf.arg_max(pred, 1), tf.argmax(y, 1)) # 按行取最大值所在的位置,比較預(yù)測(cè)結(jié)果和標(biāo)注結(jié)果是否相同,計(jì)算準(zhǔn)確率
accr = tf.reduce_mean(tf.cast(corr, "float")) # 由于一次處理一個(gè)batch,一個(gè)batch包含多條結(jié)果,求多個(gè)結(jié)果的平均值作為準(zhǔn)確度
在這一步,我們定義了模型的輸入和輸出,其中輸入有兩個(gè),一個(gè)是圖片數(shù)據(jù),一個(gè)是標(biāo)志是訓(xùn)練還是預(yù)測(cè)的標(biāo)志位is_training,訓(xùn)練和預(yù)測(cè)過(guò)程會(huì)根據(jù)這個(gè)標(biāo)志位確定是否應(yīng)用dropout,并且會(huì)影響B(tài)atch Normalization的計(jì)算。
我們定義了Loss的計(jì)算和優(yōu)化的方式,使用AdamOptimizer進(jìn)行優(yōu)化,學(xué)習(xí)率設(shè)置為0.001。除了Loss值,我們還定義了一個(gè)準(zhǔn)確率,準(zhǔn)確率是說(shuō)在1000張圖片的test過(guò)程中,我們正確識(shí)別了多少。
這里有個(gè)問(wèn)題,為什么不直接拿準(zhǔn)確率來(lái)作為優(yōu)化的目標(biāo),而是采用Loss值呢?這是因?yàn)闇?zhǔn)確率的計(jì)算對(duì)每一張圖片的識(shí)別的結(jié)果只是簡(jiǎn)單劃分為正確和錯(cuò)誤兩類(lèi),相當(dāng)于離散的概率0和1,而Loss值計(jì)算出來(lái)的概率則是一個(gè)連續(xù)區(qū)間的值。比如數(shù)字4被識(shí)別成了5或者7,對(duì)于計(jì)算準(zhǔn)確率來(lái)說(shuō)都是一樣的,都是不正確。但是對(duì)于計(jì)算Loss來(lái)說(shuō)5比7更接近4,說(shuō)明參數(shù)的調(diào)整使得模型變好了,可以繼續(xù)沿這個(gè)方向調(diào)整下去,所以這兩個(gè)參數(shù)各有各的用處。
同時(shí)還定義了用于預(yù)測(cè)過(guò)程的結(jié)果輸出out_result,并指定了節(jié)點(diǎn)名稱(chēng)name="op_to_restore",這樣可以在其他地方加載模型預(yù)測(cè)的時(shí)候知道加載哪個(gè)節(jié)點(diǎn)進(jìn)行預(yù)測(cè)結(jié)果的計(jì)算。
第三步:訓(xùn)練模型,擇優(yōu)保存
# INITIALIZER
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
print ("FUNCTIONS READY")
# 存儲(chǔ)模型路徑
savedir = "minist_model_out/"
saver = tf.train.Saver(max_to_keep=100)
save_step = 4
if not os.path.exists(savedir):
os.makedirs(savedir)
print ("SAVER READY")
# PARAMETERS
training_epochs = 50 # 在整個(gè)訓(xùn)練集上過(guò)多少遍
batch_size = 10 # 每次處理訓(xùn)練集的一個(gè)batch包含條目的數(shù)量
val_acc = 0
val_acc_max = 0
current_best_accuracy = 0.0
# OPTIMIZE
currentTime = time.time()
total_cost = 0.
total_cnt = 0
for epoch in range(training_epochs): # 循環(huán)處理所有訓(xùn)練集多次
total_batch = int(minist.train.num_examples / batch_size) # 訓(xùn)練數(shù)據(jù)集分割成若干個(gè)輸入batch,一次處理一個(gè)batch
# 循環(huán)處理所有訓(xùn)練集一次 start
for i in range(total_batch):
batch = minist.train.next_batch(batch_size) # 一次獲取batch_size個(gè)元素
batch_xs = batch[0] # 對(duì)應(yīng)一條訓(xùn)練數(shù)據(jù)的748個(gè)像素
batch_ys = batch[1] # 對(duì)應(yīng)一條訓(xùn)練數(shù)據(jù)的標(biāo)注結(jié)果
feeds = {x: batch_xs, y: batch_ys, is_training: True}
sess.run(optm, feed_dict=feeds) # 執(zhí)行一次訓(xùn)練過(guò)程
one_cost = sess.run(cost, feed_dict=feeds) # 計(jì)算本次訓(xùn)練的cost
total_cnt += 1
total_cost += one_cost
# 100步輸出一次cost結(jié)果
if total_cnt % out_frequency == 0:
print ("total_cnt:%d cost: %.9f" % (total_cnt, total_cost / out_frequency))
total_cost = 0.
# 每訓(xùn)練1000次,在測(cè)試集上測(cè)試一下
if total_cnt % test_frequency == 0:
# 在1000張測(cè)試集圖片上計(jì)算準(zhǔn)確度
val_acc_sum = 0.0
for j in range(test_photo_batch_cnt):
test_batch = minist.test.next_batch(test_photo_each_batch_size)
test_batch_xs = test_batch[0]
test_batch_ys = test_batch[1]
test_feeds = {x: test_batch_xs, y: test_batch_ys, is_training: False}
val_acc = sess.run(accr, feed_dict=test_feeds)
val_acc_sum = val_acc_sum + val_acc
val_acc = val_acc_sum / test_photo_batch_cnt
print (" 在驗(yàn)證數(shù)據(jù)集上的準(zhǔn)確度為: %.5f" % (val_acc))
# 如果準(zhǔn)確率高于之前最好水平,保存模型
if val_acc > current_best_accuracy:
current_best_accuracy = val_acc
savename = savedir + "best_cnt_" + str(total_cnt) + "_accuracy_" + str(
current_best_accuracy) + ".ckpt"
saver.save(sess=sess, save_path=savename)
print (" [%s] SAVED." % (savename))
# 循環(huán)處理所有訓(xùn)練集一次 end
這一步首先計(jì)算所有的訓(xùn)練數(shù)據(jù)有多大,根據(jù)一個(gè)batch有10條訓(xùn)練數(shù)據(jù),劃分成若干個(gè)batch,同時(shí)指定在所有訓(xùn)練數(shù)據(jù)上過(guò)多少遍(epochs),就可以循環(huán)訓(xùn)練了。
每訓(xùn)練100步輸出一下cost值,每過(guò)1000步在測(cè)試集上跑一下準(zhǔn)確度,如果高于之前最佳水平,保存之。跑完所有的遍數(shù),或是提前終止訓(xùn)練過(guò)程,模型訓(xùn)練就結(jié)束了。
第四步:加載模型,預(yù)測(cè)
訓(xùn)練過(guò)程分兩步,加載模型和預(yù)測(cè)。加載模型代碼如下:
with tf.Session() as sess:
# First let's load meta graph and restore weights
saver = tf.train.import_meta_graph('./minist_model_out/best_cnt_84000_accuracy_0.993.ckpt.meta')
saver.restore(sess, tf.train.latest_checkpoint('./minist_model_out/'))
graph = tf.get_default_graph()
x = graph.get_tensor_by_name("w1:0")
y = graph.get_tensor_by_name("w2:0")
flag = graph.get_tensor_by_name("w3:0")
# Now, access the op that you want to run.
op_to_restore = graph.get_tensor_by_name("op_to_restore:0")
通過(guò)saver.restore加載最優(yōu)的模型,加載輸入、輸出節(jié)點(diǎn),然后就可以使用模型了,可以看出我這邊預(yù)測(cè)的最終精度大約99.3%,還是很高的。
預(yù)測(cè)過(guò)程如下,對(duì)100張圖片進(jìn)行預(yù)測(cè):
for i in range(100):
batch = mnist.train.next_batch(1)
batch_xs = batch[0]
batch_ys = batch[1]
predict(batch_xs, batch_ys)
計(jì)算op_to_restore節(jié)點(diǎn),就是識(shí)別的結(jié)果,同時(shí)通過(guò)plt庫(kù)畫(huà)出進(jìn)行預(yù)測(cè)的原始圖,可以和預(yù)測(cè)結(jié)果進(jìn)行比較,整個(gè)識(shí)別過(guò)程就ok了。
def predict(val_x, labels):
feed_dict = {x: val_x, flag: False}
print "labels: "
print labels
print "predicts:"
print sess.run(op_to_restore, feed_dict)
val_x.shape = 28, 28 # nparray尺寸由1*784轉(zhuǎn)換成28*28
plt.imshow(val_x) # 顯示圖片
plt.axis('off') # 不顯示坐標(biāo)軸
plt.show()

參考:
https://stackoverflow.com/questions/36693740/whats-the-difference-between-tf-placeholder-and-tf-variable
http://geek.csdn.net/news/detail/126133
http://blog.csdn.net/mao_xiao_feng/article/details/73409975
https://morvanzhou.github.io/tutorials/machine-learning/tensorflow/5-13-A-batch-normalization/