【深度學習入門】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,則每一層大於1的梯度會不斷相乘,使梯度呈指數型增長;同理如果梯度都小於1,梯度則會逐漸趨於零;使得深度卷積網絡難以訓練。
- 訓練深層網絡時會出現退化:隨着網絡深度的增加,準確率達到飽和,然後迅速退化。
而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