基于TensorFlow Slim庫(kù)實(shí)現(xiàn)手寫(xiě)數(shù)字識(shí)別

本文介紹如何基于Tensorflow的Slim庫(kù),利用CNN(卷積神經(jīng)網(wǎng)絡(luò))實(shí)現(xiàn)手寫(xiě)數(shù)字識(shí)別。

本文GitHub源碼地址

首先介紹一些基本概念:

  1. 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:0

      a_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更常用一些。

  2. 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)。
  3. numpy庫(kù)
    一個(gè)數(shù)學(xué)庫(kù),集成了一些對(duì)數(shù)組和矩陣的操作。
  4. 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)景。
  5. 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()
預(yù)測(cè)結(jié)果展示

本文GitHub源碼地址

參考:
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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容