【深度學習入門】Paddle實現手寫數字識別詳解(基於DenseNet)

0. 閒言碎語:

OK,因爲課程需要就來做了一個手寫數字(當初就是這個小項目入的坑hahhh),因爲必須在百度的 AI Studio 上進行,所以只能用 Paddle,看了一下 Paddle 的文檔,結論是:這不就是 tensorflow + torch 的結合體嗎hahhh?所以還是比較容易寫這個數字識別的 demo的;

這裏就分享一個 Baseline 吧~

1. MNIST 數據集:

MNIST數據集包含60000個訓練集和10000測試數據集。分爲圖片和標籤,圖片是28*28的灰度圖,標籤爲0~9共10個數字。官網:http://yann.lecun.com/exdb/mnist/
在這裏插入圖片描述

2. DenseNet 詳解:

其實我以前寫過,想看更詳細的請移步:殘差神經網絡ResNet系列網絡結構詳解:從ResNet到DenseNet,這裏就簡單複述一下:

2.1 ResNet(顛覆性的殘差結構):

說到 DenseNet,就不得不提它的前身 ResNet;

論文地址:《Deep Residual Learning for Image Recognition》

LeNet 和 AlexNet的提出開啓了卷積神經網絡應用的先河,隨後的GoogleNet、VGG等網絡使用了更小的卷積核並加大了深度,證明了卷積神經網絡在處理圖像問題方面具有更加好的性能;

但是隨着層數的不斷加深,卷積神經網絡也暴露出來許多問題:

  1. 理論上講,層數越多、模型越複雜,其性能就應該越好;但是實驗證明隨着層數的不斷加深,性能反而有所下降
  2. 深度卷積網絡往往存在着梯度消失/梯度爆炸的問題;由於梯度反向傳播過程中,如果梯度都大於1,則每一層大於1的梯度會不斷相乘,使梯度呈指數型增長;同理如果梯度都小於1,梯度則會逐漸趨於零;使得深度卷積網絡難以訓練。
  3. 訓練深層網絡時會出現退化:隨着網絡深度的增加,準確率達到飽和,然後迅速退化。

而ResNet提出的殘差結構,則一定程度上緩解了模型退化和梯度消失問題:

在這裏插入圖片描述
作者提出,在一個結構單元中,如果我們想要學習的映射本來是y=H(x),那麼跟學習y=F(x)+x這個映射是等效的;這樣就將本來回歸的目標函數H(x)轉化爲F(x)+x,即F(x) = H(x) - x,稱之爲殘差。

於是,ResNet相當於將學習目標改變了,不再是學習一個完整的輸出,而是目標值H(x)和x的差值,即去掉映射前後相同的主體部分,從而突出微小的變化,也能夠將不同的特徵層融合。而且y=F(x)+x在反向傳播求導時,x項的導數恆爲1這樣也解決了梯度消失問題。

用數學表達式表示爲:

在這裏插入圖片描述

2.2 DenseNet(跨層鏈接的極致):

論文地址:《Densenet: densely connected convolutional networks》

DenseNet 的主要思想是將每一層都與後面的層都緊密(Dense)連接起來,將特徵圖重複利用,網絡更窄,參數更少,對特徵層能夠更有效地利用和傳遞,並減輕了梯度消失的問題。

網絡結構如圖:
在這裏插入圖片描述

其基本的結構單元爲:
在這裏插入圖片描述
即在一個DenseNet結構單元中,前面的特徵層會與它後面的所有特徵層相連,稱之爲Dense Block,其具體結構爲:

X —>(BN+ReLU+3x3 Conv)× 4 —> translation layer;
(BN爲batch normalization)

有文章中指出,在每3×3卷積之前可以引入1×1卷積作爲瓶頸層,可以減少輸入特徵映射的數量,從而提高計算效率。

作者就將子單元(BN-ReLU-3x3Conv)改成了bottleneck layer

BN+ReLU+1x1Conv—>BN+ReLU-3x3Conv

並且爲了解決前後特徵層深度和尺寸不同的問題,作者加入了Translation Layer

BN+Relu+1x1Conv+Pooling

3. 代碼:

代碼使用 Paddle(支持國產hahhh)

#導入需要的包
import numpy as np
import paddle as paddle
import paddle.fluid as fluid
from PIL import Image
import matplotlib.pyplot as plt
import os

numpy 是一個科學計算庫,用來做矩陣運算非常快(所以Python裏面計算的時候不要用for啦)
paddle 是百度開發的深度學習框架(也沒幾年,所以不怎麼有人用,開源社區做的也不是很好)
PIL 圖像處理庫(感覺沒有opencv-python好用)
matplotlib 一個繪圖工具(畫一些散點圖、折線圖之類的)
os 路徑和文件管理的庫(python默認安裝的)

BUF_SIZE=128 # 沒仔細查,應該是每次緩存隊列中保存數據的個數
BATCH_SIZE=32 # 批次大小

train_reader = paddle.batch(
    paddle.reader.shuffle(paddle.dataset.mnist.train(),
                          buf_size=BUF_SIZE),
    batch_size=BATCH_SIZE) # paddle 給的數據迭代器

test_reader = paddle.batch(
    paddle.reader.shuffle(paddle.dataset.mnist.test(),
                          buf_size=BUF_SIZE),
    batch_size=BATCH_SIZE)    

train_data=paddle.dataset.mnist.train();  # paddle直接給了MNIST的數據,我們直接讀取就ok

這一塊沒什麼要注意的,就是 train_reader 是訓練集迭代器,test_reader 是測試集迭代器;

# 定義DenseNet

class DenseNet(): 
    def __init__(self, layers, dropout_prob):
        self.layers = layers
        self.dropout_prob = dropout_prob

    def bottleneck_layer(self, input, fliter_num, name):
        bn = fluid.layers.batch_norm(input=input, act='relu', name=name + '_bn1')
        conv1 = fluid.layers.conv2d(input=bn, num_filters=fliter_num * 4, filter_size=1, name=name + '_conv1')
        dropout = fluid.layers.dropout(x=conv1, dropout_prob=self.dropout_prob)

        bn = fluid.layers.batch_norm(input=dropout, act='relu', name=name + '_bn2')
        conv2 = fluid.layers.conv2d(input=bn, num_filters=fliter_num, filter_size=3, padding=1, name=name + '_conv2')
        dropout = fluid.layers.dropout(x=conv2, dropout_prob=self.dropout_prob)

        return dropout

    def dense_block(self, input, block_num, fliter_num, name):
        layers = []
        layers.append(input)#拼接到列表

        x = self.bottleneck_layer(input, fliter_num, name=name + '_bottle_' + str(0))
        layers.append(x)
        for i in range(block_num - 1):
            x = paddle.fluid.layers.concat(layers, axis=1)
            x = self.bottleneck_layer(x, fliter_num, name=name + '_bottle_' + str(i + 1))
            layers.append(x)

        return paddle.fluid.layers.concat(layers, axis=1)

    def transition_layer(self, input, fliter_num, name):
        bn = fluid.layers.batch_norm(input=input, act='relu', name=name + '_bn1')
        conv1 = fluid.layers.conv2d(input=bn, num_filters=fliter_num, filter_size=1, name=name + '_conv1') 
        dropout = fluid.layers.dropout(x=conv1, dropout_prob=self.dropout_prob)

        return fluid.layers.pool2d(input=dropout, pool_size=2, pool_type='avg', pool_stride=2) 
    def net(self, input, class_dim=1000): 

        layer_count_dict = {
            121: (32, [3, 3, 6])
        }
        layer_conf = layer_count_dict[self.layers]

        conv = fluid.layers.conv2d(input=input, num_filters=layer_conf[0] * 2, 
            filter_size=3, name='densenet_conv0')
        conv = fluid.layers.pool2d(input=conv, pool_size=2, pool_padding=1, pool_type='max', pool_stride=2)
        for i in range(len(layer_conf[1]) - 1):
            conv = self.dense_block(conv, layer_conf[1][i], layer_conf[0], 'dense_' + str(i))
            conv = self.transition_layer(conv, layer_conf[0], name='trans_' + str(i))

        conv = self.dense_block(conv, layer_conf[1][-1], layer_conf[0], 'dense_' + str(len(layer_conf[1])))
        conv = fluid.layers.pool2d(input=conv, global_pooling=True, pool_type='avg')
        out = fluid.layers.fc(conv, class_dim, act='softmax')

        return out

這當然不是我寫的啦,,,網上找的輪子,其實一般來說圖像識別這邊不需要自己造輪子的,這個任務還比較簡單,如果你想去寫一個語義分割、目標檢測的話,自己造輪子不僅浪費時間,而且命名空間會很麻煩,網上的預訓練模型可能用不了;

總之這裏定義個一個 DenseNet 類,並且把原來的 121 層改成了 9 層(畢竟這個任務相比 ImageNet 的 1000 分類+224 大小圖像來說比較簡單,太複雜很容易過擬合併且訓練滿),通過 .net() 方法輸出預測;

# 定義輸入輸出層
image = fluid.layers.data(name='image', shape=[1, 28, 28], dtype='float32')#單通道,28*28像素值
label = fluid.layers.data(name='label', shape=[1], dtype='int64') # 圖片標籤
# 獲取分類器
model = DenseNet(121, 0.5)
out = model.net(input=image, class_dim=10)
# 獲取損失函數和準確率函數
cost = fluid.layers.cross_entropy(input=out, label=label)  #使用交叉熵損失函數,描述真實樣本標籤和預測概率之間的差值
avg_cost = fluid.layers.mean(cost)
acc = fluid.layers.accuracy(input=out, label=label) # 定義準確率

這裏也沒啥好說的,都有註釋;

唯一要注意的一點是,Paddle 裏面的 fluid.layers.cross_entropy 中 label 不需要改成跟預測一樣的向量形式,跟 TF 的 tf.nn.sparse_softmax_cross_entropy_with_logits() 類似;

# 定義優化方法
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=2e-2)   #使用Adam算法進行優化
opts = optimizer.minimize(avg_cost)
# 定義一個使用CPU的解析器
place = fluid.CPUPlace()
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())

feeder = fluid.DataFeeder(place=place, feed_list=[image, label])

這裏跟那個 TF 是一模一樣了,定義了一個 Adam 優化器,然後配置了訓練使用 CPU;

feeder 就是傳入數據的一個東東;


all_train_iter=0
all_train_iters=[]
all_train_costs=[]
all_train_accs=[]

def draw_train_process(title,iters,costs,accs,label_cost,lable_acc):
    plt.title(title, fontsize=24)
    plt.xlabel("iter", fontsize=20)
    plt.ylabel("cost/acc", fontsize=20)
    plt.plot(iters, costs,color='red',label=label_cost) 
    plt.plot(iters, accs,color='green',label=lable_acc) 
    plt.legend()
    plt.grid()
    plt.show()

然後寫了幾個列表變量,保存訓練的結果;

定義了 draw_train_process 函數,用來畫 loss 跟 acc 的折線圖;

EPOCH_NUM=20  # 調參 訓練輪數
model_save_dir = "/home/aistudio/data/hand.inference.model"
for pass_id in range(EPOCH_NUM):
    # 進行訓練
    for batch_id, data in enumerate(train_reader()):                         #遍歷train_reader
        train_cost, train_acc = exe.run(program=fluid.default_main_program(),#運行主程序
                                        feed=feeder.feed(data),              #給模型喂入數據
                                        fetch_list=[avg_cost, acc])          #fetch 誤差、準確率                                          
        all_train_iter=all_train_iter+1
        all_train_iters.append(all_train_iter)
        all_train_costs.append(train_cost[0])
        all_train_accs.append(train_acc[0])        
        # 每100個batch打印一次信息  誤差、準確率
        if batch_id % 100 == 0:
            print('Pass:%d, Batch:%d, Cost:%0.5f, Accuracy:%0.5f' %
                  (pass_id, batch_id, train_cost[0], train_acc[0]))

    # 進行測試
    test_accs = []
    test_costs = []
    #每訓練一輪 進行一次測試
    for batch_id, data in enumerate(test_reader()):                         #遍歷test_reader
        test_cost, test_acc = exe.run(program=fluid.default_main_program(), #執行訓練程序
                                      feed=feeder.feed(data),               #喂入數據
                                      fetch_list=[avg_cost, acc])           #fetch 誤差、準確率
        test_accs.append(test_acc[0])                                       #每個batch的準確率
        test_costs.append(test_cost[0])                                     #每個batch的誤差                              
    # 求測試結果的平均值
    test_cost = (sum(test_costs) / len(test_costs))                         #每輪的平均誤差
    test_acc = (sum(test_accs) / len(test_accs))                            #每輪的平均準確率
    print('Test:%d, Cost:%0.5f, Accuracy:%0.5f' % (pass_id, test_cost, test_acc))    
    #保存模型
    # 如果保存路徑不存在就創建
    if not os.path.exists(model_save_dir):
        os.makedirs(model_save_dir)
    print ('save models to %s' % (model_save_dir))
    fluid.io.save_inference_model(model_save_dir,  #保存推理model的路徑
                                  ['image'],       #推理(inference)需要 feed 的數據
                                  [out],       #保存推理(inference)結果的 Variables
                                  exe)             #executor 保存 inference model
draw_train_process("training",all_train_iters,all_train_costs,all_train_accs,"trainning cost","trainning acc")

這裏運行就可以開始開心地:訓練-測試-訓練-測試,,,,,

在這裏插入圖片描述
在這裏插入圖片描述
呼…只用 CPU 還真是慢啊,,,

大概最後 99.532% 的準確率(其實簡單的 2 ~ 3 層的卷積網絡也能達到這個水平,,,就當科普一下 DenseNet 嘍);

def load_image(file):
    im = Image.open(file).convert('L')                        #將RGB轉化爲灰度圖像,L代表灰度圖像,像素值在0~255之間
    im = im.resize((28, 28), Image.ANTIALIAS)                 #resize image with high-quality 圖像大小爲28*28

    im = np.array(im).reshape(1, 1, 28, 28).astype(np.float32)#返回新形狀的數組,把它變成一個 numpy 數組以匹配數據饋送格式。
    #print(im)
    im = im / 255.0 * 2.0 - 1.0                               #歸一化到【-1~1】之間
    return im

def load_image_cv(path):
    im = cv2.imread(path, 0)
    im = cv2.resize(im, (28, 28)).astype(np.float)
    im = im / 255.0 * 2.0 - 1.0
    return im

這裏定義一下加載圖片的函數(我順便再寫一個cv2的吧,是不是更簡潔呀哈哈);

infer_path='/home/aistudio/work/data5435/infer_9.jpg'
img = Image.open(infer_path)
plt.imshow(img)   #根據數組繪製圖像
plt.show()        #顯示圖像
infer_exe = fluid.Executor(place)
#聲明一個新的作用域
inference_scope = fluid.core.Scope()

然後讀取一下用來測試的那張圖片(路徑可以改成你自己的),並聲明一個新的作用域用來測試(感覺跟Session一樣的東西);

#運行時中的所有變量都將分配給新的scope
with fluid.scope_guard(inference_scope):
    #獲取訓練好的模型
    #從指定目錄中加載模型
    [inference_program,                                            #推理Program
     feed_target_names,                                            #是一個str列表,它包含需要在推理 Program 中提供數據的變量的名稱。 
     fetch_targets] = fluid.io.load_inference_model(model_save_dir,#fetch_targets:是一個列表,從中我們可以得到推斷結果。model_save_dir:模型保存的路徑
                                                    infer_exe)     #infer_exe: 運行 inference model的 executor
    infer_path='/home/aistudio/work/data5286/infer_3.png'
    img = load_image(infer_path)

    results = infer_exe.run(program=inference_program,         #運行推測程序
                   feed={feed_target_names[0]: img},           #喂入要預測的img
                   fetch_list=fetch_targets)                   #得到推測結果,  
    # 獲取概率最大的label
    print(results)
    lab = np.argsort(results)                                  #argsort函數返回的是result數組值從小到大的索引值
    print(lab)
    print("該圖片的預測結果的label爲: %d" % lab[0][0][-1])     #-1代表讀取數組中倒數第一列
    img = Image.open(infer_path)
    plt.title('pred:'+str(lab[0][0][-1]))  
    plt.imshow(img)
    plt.title()

ok 測試完畢~
在這裏插入圖片描述

最後歡迎關注我的個人博客哦:https://blog.csdn.net/weixin_44936889

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章