【飛槳開發者說】鐘山,中科院信工所工程師,主要研究計算機視覺、深度學習。
想必很多人都對自己的顏值到底怎樣充滿好奇,也有很多軟體為大家提供了顏值打分的趣味功能。其實,顏值打分也可以視為一個圖像分類問題,今天就向大家介紹如何利用飛槳搭建一個 VGG 網絡,實現一個簡單的顏值打分 demo。
VCGNet 介紹
VGGNet 由牛津大學的視覺幾何組(Visual Geometry Group)和 Google DeepMind 公司提出,是 ILSVRC-2014 中定位任務第一名和分類任務第二名。提出 VGGNet 的主要目的是探究在大規模圖像識別任務中,卷積網絡深度對模型精確度的影響。
通過 VGGNet,研究人員證明了基於尺寸較小的卷積核,增加網絡深度可以有效提升模型的效果。VGGNet 結構簡單,模型的泛化能力好,因此受到研究人員青睞而被廣泛使用,到現在依然經常被用作圖像特徵提取。
VGGNet 引入「模塊化」的設計思想,將不同的層進行簡單的組合構成網絡模塊,再用模塊來組裝完整網絡,而不再是以「層」為單元組裝網絡。VGGNet 有5種不同的 VGGNet 配置(如上表所示),其中每一列代表一種網絡配置,分別用 A~E 來表示。
從表格中可以看出所有 VGG 配置都有五個卷積模塊,模塊中所有卷積都是3×3卷積核(conv3),因此特徵圖的尺寸在模塊內不是變的,每個模塊卷積之後緊接著最大池化層。最淺的A網絡也稱為 VGG-11,它包含11個帶可學習參數層,最深的E網絡也稱為 VGG-19,包含19個帶可學習參數層。
業界普遍認為,更深的網絡具有比淺網絡更強的表達能力,更能刻畫現實,完成更複雜的任務,VGG-19結構如下圖所示
VGGNet 是基於 AlexNet 網絡的,VGG 在 Alexnet(註:Alexnet 介紹參考《深度學習導論與應用實踐》5.7.2節內容)基礎上對深度神經網絡在深度和寬度上做了更多深入的研究,相較於 Alexnet 而言,VGG 中的3×3卷積核的感受野要小,但是兩個3×3 卷積的卷積級聯其感受野相當於5×5卷積,三個3×3卷積級聯感受野相當於7×7卷積,為什麼要這樣做呢?
首先,每個卷積層後面都跟隨著非線性激活層,這樣整合三個卷積層和三個非線性激活層增加了非線性表達能力,提高網絡的判斷能力。
其次是減少了網絡的參數,假設輸入特徵圖的通道數為
輸出特徵圖的通道數為
級聯三個3×3卷積層中的參數為
而使用7×7卷積層中的參數為
參數要多81%。
此外 C 網絡中還使用了3×3卷積層,同理也是為了增加非線性與減少網絡參數。
介紹完 VGG 網絡結構以及相關基礎之後,接下來我們將在 AI Studio 平臺上用 Paddle Fluid API 搭建一個 VGG 網絡模型,實現對顏值打分的功能。
本實踐代碼運行的環境配置如下:Python 版本為3.7,飛槳版本為1.6.0,電腦配置 MacOS 10.15。
數據準備
首先,我們需要準備用於模型訓練的數據。這次我們用到的是華南理工大學實驗室公布的人臉照片數據集(https://github.com/HCIILAB/SCUT-FBP5500-Database-Release),該數據中包含了500張不同女生的彩色照片以及每張照片的顏值得分。其中,得分是由隨機尋找的70人對每張圖片進行打分,最終求平均得出。在本次實踐中,我們將顏值得分全部設定在1-5分並取整,即每個圖片的標籤為[1,2,3,4,5]中的一個。
註:原始數據集中每張圖像並沒有專門對人臉部分進行剪裁,圖像中包含大量背景。因此,我們將提前去除了非人臉的部分並將圖片大小剪裁為224x224,存儲為本次實踐中使用的數據集,如下圖所示,圖片命名格式是:顏值得分-圖片編號。處理好的數據集獲取地址為
https://pan.baidu.com/s/1B9Hlo031BABA6Hj-r16EQw。
了解了數據的基本信息之後,我們需要有一個用於獲取圖片數據的數據提供器,在這裡我們定義為 data_reader(),它的作用就是提供圖片以及圖片的標籤(即顏值得分)。為了方便對圖片進行歸一化處理和獲得數據的 label,我們定義了 data_mapper()。
注意:由於飛槳中交叉熵損失函數要求 label 必須從0開始,而數據集 label 從1開始,所以在獲取 label 時,我們將 label 減1。
#導入必要的包
import os
import paddle
import numpy as np
from PIL importImage
import paddle.fluidas fluid
frommultiprocessing import cpu_count
importmatplotlib.pyplot as plt
def data_mapper(data):
img, label = data
img = paddle.dataset.image.load_image(img)
#將img數組進行進行歸一化處理,得到0到1之間的數值
img= img.flatten().astype('float32')/255.0
return img, int(label)
def data_reader(data_path,buffered_size=512):
print(data_path)
def reader():
for image in os.listdir(data_path):
label = int(image.split('-')[0])-1 #label減1
img = os.path.join(data_path+ '/' +image)
yield img, label
returnpaddle.reader.xmap_readers(data_mapper, reader, cpu_count(), buffered_size)
paddle.reader.xmap_readers() 是飛槳提供的一個方法,功能是多線程下,使用自定義映射器 reader 返回樣本到輸出隊列。
詳細介紹:
https://www.paddlepaddle.org.cn/documentation/docs/zh/api_cn/io_cn/xmap_readers_cn.html#xmap-readers
有了數據提供器 data_reader()後,我們就可以很簡潔地得到用於訓練的數據提供器 train_reader()和用於測試的數據提供提供器 test_reader(),BATCH_SIZE 是一個批次的大小,在這裡我們設定為16。
#構造訓練、測試數據提供器
BATCH_SIZE = 16
train_r =data_r(data_path='/home/aistudio/face_image_train')
train_reader =paddle.batch(paddle.reader.shuffle(reader=train_r,buf_size=128),
batch_size=BATCH_SIZE)
test_r=data_r(data_path='/home/aistudio/face_image_test')
test_reader = paddle.batch(test_r,batch_size=BATCH_SIZE
網絡配置
數據準備的工作完成之後,我們開始構造前面已經介紹過的 VGG-16 網絡,網絡結構圖如下:
首先用 fluid.nets.img_conv_group()函數定義一個卷積模塊 conv_block(),從而通過配置卷積模塊組件 VGG-16 網絡結構。卷積模塊 conv_block()主要用來設定該模塊中的輸入、池化、卷積、激活函數、以及 dropout 和 bacthnorm。
獲得了卷積模塊後,通過多個卷積模塊的組合以及三個全連接層就可以輕鬆獲得一個 VGG-16 模型。
def vgg_bn_drop(image, type_size):
def conv_block(ipt, num_filter, groups,dropouts):
return fluid.nets.img_conv_group(
input=ipt, # 具有[N,C,H,W]格式的輸入圖像
pool_size=2,
pool_stride=2,
conv_num_filter=[num_filter] *groups, # 過濾器個數
conv_filter_size=3, # 過濾器大小
conv_act='relu',
conv_with_batchnorm=True, # 表示在 Conv2d Layer 之後是否使用 BatchNorm
conv_batchnorm_drop_rate=dropouts,#表示 BatchNorm 之後的 Dropout Layer 的丟棄概率
pool_type='max') # 最大池化
conv1 = conv_block(image, 64, 2, [0.0, 0])
conv2 = conv_block(conv1, 128, 2, [0.0, 0])
conv3 = conv_block(conv2, 256, 3, [0.0,0.0, 0])
conv4 = conv_block(conv3, 512, 3, [0.0,0.0, 0])
conv5 = conv_block(conv4, 512, 3, [0.0,0.0, 0])
drop = fluid.layers.dropout(x=conv2,dropout_prob=0.5)
fc1 = fluid.layers.fc(input=drop, size=512,act=None)
bn = fluid.layers.batch_norm(input=fc1, act='relu')
drop2 = fluid.layers.dropout(x=bn,dropout_prob=0.5)
fc2 = fluid.layers.fc(input=drop2, size=1024,act=None)
predict = fluid.layers.fc(input=fc2,size=type_size, act='softmax')
return predict
接下來進行數據層的定義。由於數據是224x224的三通道彩色圖像,所以輸入層 image 的維度為[None,3,224,224],label 代表圖片的顏值得分標籤。
# 定義輸入輸出層
# 定義兩個張量
image =fluid.layers.data(name='image', shape=[3, 224, 224], dtype='float32')
label =fluid.layers.data(name='label', shape=[1], dtype='int64')
上面我們定義好了 VGG 網絡結構,這裡我們使用定義好的網絡來獲取分類器。
# 獲取分類器predict=vgg_bn_drop(image,5)
接著是定義損失函數,這裡使用的是交叉熵損失函數,該函數在分類任務上比較常用。定義了一個損失函數之後,還要對它求平均值,因為定義的是一個 Batch 的損失值。同時還可以定義一個準確率函數,可以在訓練的時候輸出分類的準確率。
# 定義損失函數和準確率函數
cost =fluid.layers.cross_entropy(input=predict, label=label)
avg_cost =fluid.layers.mean(cost)
accuracy =fluid.layers.accuracy(input=predict, label=label)
為了區別測試和訓練,在這裡我們克隆一個 test_program()。
# 克隆main_program得到test_program,使用參數for_test來區分該程序是用來訓練還是用來測試#注意:該fluid.default_main_program().clone()請在optimization之前使用.test_program =fluid.default_main_program().clone(for_test=True)接著定義優化算法,這裡使用的是Adam優化算法,指定學習率為0.002。
# 定義優化方法
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=0.002)
opts = optimizer.minimize(avg_cost)
用戶完成網絡定義後,一段 Fluid 程序中通常存在兩個 Program:
(1)fluid.default_startup_program:定義了創建模型參數,輸入輸出,以及模型中可學習參數的初始化等各種操作,由框架自動生成,使用時無需顯示地創建;
(2)fluid.default_main_program :定義了神經網絡模型,前向反向計算,以及優化算法對網絡中可學習參數的更新,使用 Fluid 的核心就是構建起 default_main_program。
模型訓練
在上一步驟中定義好了網絡模型,即構造好了兩個核心 Program,接下來將介紹飛槳如何使用 Excutor 來執行 Program:
#定義使用CPU還是GPU,使用CPU時use_cuda = False,使用GPU時use_cuda = True
use_cuda = False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
#創建一個Executor實例exe
exe =fluid.Executor(place)
#正式進行網絡訓練前,需先執行參數初始化
exe.run(fluid.default_startup_program())
定義好網絡訓練需要的 Executor,在執行訓練之前,需要告知網絡傳入的數據分為兩部分,第一部分是 images 值,第二部分是 label 值:
feeder = fluid.DataFeeder(place=place, feed_list=[image, label])
之後就可以進行正式的訓練了,本實踐中設置訓練輪數60。在 Executor 的 run 方法中,feed 代表以字典的形式定義了數據傳入網絡的順序,feeder在上述代碼中已經進行了定義,將 data[0]、data[1]分別傳給 image、label。fetch_list 定義了網絡的輸出。
在每輪訓練中,每10個 batch,列印一次訓練平均誤差和準確率。每輪訓練完成後,使用驗證集進行一次驗證。
EPOCH_NUM = 60
#訓練過程數據記錄
all_train_iter=0
all_train_iters=[]
all_train_costs=[]
all_train_accs=[]
#測試過程數據記錄
all_test_iter=0
all_test_iters=[]
all_test_costs=[]
all_test_accs=[]
model_save_dir ="/home/aistudio/work"
for pass_id inrange(EPOCH_NUM):
# 開始訓練
for batch_id, data inenumerate(train_reader()): #遍歷訓練集,並為數據加上索引batch_id
train_cost,train_acc =exe.run(program=fluid.default_main_program(),#運行主程序
feed=feeder.feed(data), #餵入一個batch的數據
fetch_list=[avg_cost, acc]) #fetch均方誤差和準確率
all_train_iter=all_train_iter+BATCH_SIZE
all_train_iters.append(all_train_iter)
all_train_costs.append(train_cost[0])
all_train_accs.append(train_acc[0])
#每10次batch列印一次訓練、進行一次測試
if batch_id % 10== 0:
print('Pass:%d, Batch:%d,Cost:%0.5f, Accuracy:%0.5f' %
(pass_id, batch_id, train_cost[0],train_acc[0]))
# 開始測試
test_costs = [] #測試的損失值
test_accs = [] #測試的準確率
for batch_id, data in enumerate(test_reader()):
test_cost, test_acc =exe.run(program=test_program, #執行訓練程序
feed=feeder.feed(data), #餵入數據
fetch_list=[avg_cost, acc]) #fetch 誤差、準確率
test_costs.append(test_cost[0]) #記錄每個batch的誤差
test_accs.append(test_acc[0]) #記錄每個batch的準確率
all_test_iter=all_test_iter+BATCH_SIZE
all_test_iters.append(all_test_iter)
all_test_costs.append(test_cost[0])
all_test_accs.append(test_acc[0])
# 求測試結果的平均值
test_cost = (sum(test_costs) /len(test_costs)) #計算誤差平均值(誤差和/誤差的個數)
test_acc = (sum(test_accs) /len(test_accs)) #計算準確率平均值( 準確率的和/準確率的個數)
print('Test:%d, Cost:%0.5f, ACC:%0.5f' %(pass_id, test_cost, test_acc))
每輪訓練完成後,對模型進行一次保存,使用飛槳提供的fluid.io.save_inference_model()進行模型保存:
# 保存模型
# 如果保存路徑不存在就創建
if not os.path.exists(model_save_dir):
os.makedirs(model_save_dir)
print('savemodels to %s' % (model_save_dir))
fluid.io.save_inference_model(model_save_dir, # 保存預測Program的路徑
['image'], #預測需要feed的數據
[predict], #保存預測結果
exe) #executor 保存預測模型
模型評估
在這裡我們定義了 draw_cost_process()和 draw_acc_process()函數,用來可視化展示訓練過程中的損失和準確率的變化趨勢。
def draw_cost_process(title,iters,costs,label_cost): plt.title(title, fontsize=24) plt.xlabel("iter", fontsize=20) plt.ylabel("cost", fontsize=20) plt.plot(iters,costs,color='red',label=label_cost) plt.legend() plt.grid() plt.show()defdraw_acc_process(title,iters,acc,label_acc): plt.title(title, fontsize=24) plt.xlabel("iter", fontsize=20) plt.ylabel("acc", fontsize=20) plt.plot(iters,acc,color='green',label=label_acc) plt.legend() plt.grid() plt.show()
調用以下兩條繪製曲線,方便觀察迭代過程中的變化趨勢,從而對網絡訓練結果進行評估。
①draw_acc_process()
②draw_cost_process()
#調用繪製曲線
draw_acc_process("training",all_train_iters, all_train_accs, "trainning acc")
draw_acc_process("testing",all_test_iters, all_test_accs, "test acc")
draw_cost_process("training",all_train_iters, all_train_costs, "trainning acc")
draw_cost_process("testing",all_test_iters, all_test_costs, "test acc")
從訓練結果來看,模型在測試集上的準確率有一定的提升,損失也在不斷下降。後續大家可以根據訓練結果調整模型參數,對模型進行優化,也可以使用 CNN 模型來進行對比。
模型預測
前面已經進行了模型訓練,並保存了訓練好的模型。接下來就可以使用訓練好的模型對手寫數字圖片進行識別了。
預測之前必須要對預測的圖像進行預處理,首先對輸入的圖片進行灰度化,然後壓縮圖像大小為224x224,接著將圖像轉換成一維向量,最後對一維向量進行歸一化處理。代碼實現如下所示:
#圖片預處理defload_image(file): im = Image.open(file) im =im.resize((224, 224), Image.ANTIALIAS) #resize 圖像大小為224*224 if np.array(im).shape[2] == 4 #判斷圖像通道數,若為4通道,則將其轉化為3通道 im = np.array(im)[:,:,:3] im = np.array(im).reshape(1, 3, 224,224).astype(np.float32) #返回新形狀的數組,把它變成一個 numpy 數組以匹配數據饋送格式。im = im / 255.0 #歸一化到[-1~1]之間 return im
接下來使用訓練好的模型對經過預處理的圖片進行預測。首先從指定目錄中加載訓練好的模型,然後餵入要預測的圖片向量,返回模型的輸出結果,即為預測概率,這些概率的總和為1。
# 加載模型並開始預測
infer_exe =fluid.Executor(place)
infer_img='/home/aistudio/image1.jpg'
#獲取訓練好的模型
#從指定目錄中加載 推理model(inference model)
[inference_program,#預測用的program
feed_target_names,#是一個str列表,它包含需要在推理 Program 中提供數據的變量的名稱。
fetch_targets] = fluid.io.load_inference_model(model_save_dir,infer_exe)#fetch_targets:是一個 Variable 列表,從中我們可以得到推斷結果。
img =Image.open(infer_img)
plt.imshow(img) #根據數組繪製圖像
plt.show() #顯示圖像
image=load_image(infer_img)
# 開始預測
results =infer_exe.run(
inference_program, #運行預測程序
feed={feed_target_names[0]: image},#餵入要預測的數據
fetch_list=fetch_targets) #得到推測結果
得到各個標籤的概率值後,獲取概率最大的標籤,即為顏值的預測結果。
# 獲取概率最大的labelprint('results',results)label_list =["1", "2", "3", "4", "5"]print("inferresults: %s" % label_list[np.argmax(results[0])])
運行結果如下:
results[array([[1.7734790e-12, 8.6307369e-02, 1.1037191e-03, 9.1258878e-01,
7.8247488e-08]], dtype=float32)]infer results: 4
最終我們的美女同事顏值被評估為4分!
至此,恭喜您!已經成功使用飛槳搭建了一個簡單的卷積神經網絡並實現了顏值打分。
如果您還想嘗試更多,可以從官網繼續閱讀相關的文檔及更多豐富的模型實例。
或者您可以參考由中國科學院大學專家、百度深度學習工程師團隊共同編著,清華大學出版社出版的《深度學習導論與應用實踐》,該教材理論、實踐深度結合,源於開源深度學習框架飛槳,獨家融合大量實踐案例,希望能幫助到想深入了解深度學習或者想進行深度學習實踐的您!