基礎入門,怎樣用PaddlePaddle優雅地寫VGG與ResNet
綜述
圖像相比文字能夠提供更加生動、容易理解及更具藝術感的信息,圖像分類是根據圖像的語義信息將不同類別圖像區分開來,是圖像檢測、圖像分割、物體跟蹤、行為分析等其他高層視覺任務的基礎。圖像分類在安防、交通、互聯網、醫學等領域有著廣泛的應用。
一般來說,圖像分類通過手工提取特徵或特徵學習方法對整個圖像進行全部描述,然後使用分類器判別物體類別,因此如何提取圖像的特徵至關重要。基於深度學習的圖像分類方法,可以通過有監督或無監督的方式學習層次化的特徵描述,從而取代了手工設計或選擇圖像特徵的工作。
深度學習模型中的卷積神經網路(Convolution Neural Network, CNN) 直接利用圖像像素信息作為輸入,最大程度上保留了輸入圖像的所有信息,通過卷積操作進行特徵的提取和高層抽象,模型輸出直接是圖像識別的結果。這種基於"輸入-輸出"直接端到端的學習方法取得了非常好的效果。
本教程主要介紹圖像分類的深度學習模型,以及如何使用PaddlePaddle在CIFAR10數據集上快速實現CNN模型。
項目地址:
http://paddlepaddle.org/documentation/docs/zh/1.3/beginners_guide/basics/image_classification/index.html
基於ImageNet數據集訓練的更多圖像分類模型,及對應的預訓練模型、finetune操作詳情請參照Github:
https://github.com/PaddlePaddle/models/blob/develop/PaddleCV/image_classification/README_cn.md
效果
圖像分類包括通用圖像分類、細粒度圖像分類等。圖1展示了通用圖像分類效果,即模型可以正確識別圖像上的主要物體。
圖1. 通用圖像分類展示
圖2展示了細粒度圖像分類-花卉識別的效果,要求模型可以正確識別花的類別。
圖2. 細粒度圖像分類展示
一個好的模型既要對不同類別識別正確,同時也應該能夠對不同視角、光照、背景、變形或部分遮擋的圖像正確識別(這裡我們統一稱作圖像擾動)。圖3展示了一些圖像的擾動,較好的模型會像聰明的人類一樣能夠正確識別。
圖3. 擾動圖片展示[7]
模型概覽:CNN
傳統CNN包含卷積層、全連接層等組件,並採用softmax多類別分類器和多類交叉熵損失函數,一個典型的卷積神經網路如圖4所示,我們先介紹用來構造CNN的常見組件。
圖4. CNN網路示例[5]
? 卷積層(convolution layer): 執行卷積操作提取底層到高層的特徵,發掘出圖片局部關聯性質和空間不變性質。
? 池化層(pooling layer): 執行降採樣操作。通過取卷積輸出特徵圖中局部區塊的最大值(max-pooling)或者均值(avg-pooling)。降採樣也是圖像處理中常見的一種操作,可以過濾掉一些不重要的高頻信息。
? 全連接層(fully-connected layer,或者fc layer): 輸入層到隱藏層的神經元是全部連接的。
? 非線性變化: 卷積層、全連接層後面一般都會接非線性變化函數,例如Sigmoid、Tanh、ReLu等來增強網路的表達能力,在CNN里最常使用的為ReLu激活函數。
? Dropout [1] : 在模型訓練階段隨機讓一些隱層節點權重不工作,提高網路的泛化能力,一定程度上防止過擬合。
接下來我們主要介紹VGG,ResNet網路結構。
1、VGG
牛津大學VGG(Visual Geometry Group)組在2014年ILSVRC提出的模型被稱作VGG模型[2] 。該模型相比以往模型進一步加寬和加深了網路結構,它的核心是五組卷積操作,每兩組之間做Max-Pooling空間降維。同一組內採用多次連續的3X3卷積,卷積核的數目由較淺組的64增多到最深組的512,同一組內的卷積核數目是一樣的。卷積之後接兩層全連接層,之後是分類層。由於每組內卷積層的不同,有11、13、16、19層這幾種模型,下圖展示一個16層的網路結構。
VGG模型結構相對簡潔,提出之後也有很多文章基於此模型進行研究,如在ImageNet上首次公開超過人眼識別的模型[4]就是借鑒VGG模型的結構。
圖5. 基於ImageNet的VGG16模型
2、ResNet
ResNet(Residual Network) [3] 是2015年ImageNet圖像分類、圖像物體定位和圖像物體檢測比賽的冠軍。針對隨著網路訓練加深導致準確度下降的問題,ResNet提出了殘差學習方法來減輕訓練深層網路的困難。在已有設計思路(BN, 小卷積核,全卷積網路)的基礎上,引入了殘差模塊。每個殘差模塊包含兩條路徑,其中一條路徑是輸入特徵的直連通路,另一條路徑對該特徵做兩到三次卷積操作得到該特徵的殘差,最後再將兩條路徑上的特徵相加。
殘差模塊如圖7所示,左邊是基本模塊連接方式,由兩個輸出通道數相同的3x3卷積組成。右邊是瓶頸模塊(Bottleneck)連接方式,之所以稱為瓶頸,是因為上面的1x1卷積用來降維(圖示例即256->64),下面的1x1卷積用來升維(圖示例即64->256),這樣中間3x3卷積的輸入和輸出通道數都較小(圖示例即64->64)。
圖7. 殘差模塊
3、數據準備
由於ImageNet數據集較大,下載和訓練較慢,為了方便大家學習,我們使用CIFAR10數據集。CIFAR10數據集包含60,000張32x32的彩色圖片,10個類別,每個類包含6,000張。其中50,000張圖片作為訓練集,10000張作為測試集。圖11從每個類別中隨機抽取了10張圖片,展示了所有的類別。
圖11. CIFAR10數據集[6]
Paddle API提供了自動載入cifar數據集模塊paddle.dataset.cifar。
通過輸入python train.py,就可以開始訓練模型了,以下小節將詳細介紹train.py的相關內容。
模型結構
1、Paddle 初始化
讓我們從導入Paddle Fluid API 和輔助模塊開始。
from __future__ import print_function
import os
import paddle
import paddle.fluidas fluid
import numpy
import sys
from vgg import vgg_bn_drop
from resnet import resnet_cifar10
本教程中我們提供了VGG和ResNet兩個模型的配置。
2、VGG
首先介紹VGG模型結構,由於CIFAR10圖片大小和數量相比ImageNet數據小很多,因此這裡的模型針對CIFAR10數據做了一定的適配。卷積部分引入了BN和Dropout操作。VGG核心模塊的輸入是數據層,vgg_bn_drop定義了16層VGG結構,每層卷積後面引入BN層和Dropout層,詳細的定義如下:
def vgg_bn_drop(input):
def conv_block(ipt, num_filter, groups, dropouts):
return fluid.nets.img_conv_group(
input=ipt,
pool_size=2,
pool_stride=2,
conv_num_filter=[num_filter] * groups,
conv_filter_size=3,
conv_act="relu",
conv_with_batchnorm=True,
conv_batchnorm_drop_rate=dropouts,
pool_type="max")
conv1= conv_block(input, 64, 2, [0.3, 0])
conv2= conv_block(conv1, 128, 2, [0.4, 0])
conv3= conv_block(conv2, 256, 3, [0.4, 0.4, 0])
conv4= conv_block(conv3, 512, 3, [0.4, 0.4, 0])
conv5= conv_block(conv4, 512, 3, [0.4, 0.4, 0])
drop= fluid.layers.dropout(x=conv5, 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=512, act=None)
predict= fluid.layers.fc(input=fc2, size=10, act="softmax")
return predict
首先定義了一組卷積網路,即conv_block。卷積核大小為3x3,池化窗口大小為2x2,窗口滑動大小為2,groups決定每組VGG模塊是幾次連續的卷積操作,dropouts指定Dropout操作的概率。所使用的img_conv_group是在paddle.fluit.net中預定義的模塊,由若干組Conv->BN->ReLu->Dropout 和一組Pooling 組成。
五組卷積操作,即5個conv_block。第一、二組採用兩次連續的卷積操作。第三、四、五組採用三次連續的卷積操作。每組最後一個卷積後面Dropout概率為0,即不使用Dropout操作。
最後接兩層512維的全連接。
在這裡,VGG網路首先提取高層特徵,隨後在全連接層中將其映射到和類別維度大小一致的向量上,最後通過Softmax方法計算圖片劃為每個類別的概率。
3、ResNet
ResNet模型的第1、3、4步和VGG模型相同,這裡不再介紹。主要介紹第2步即CIFAR10數據集上ResNet核心模塊。
先介紹resnet_cifar10中的一些基本函數,再介紹網路連接過程。
? conv_bn_layer: 帶BN的卷積層。
? shortcut: 殘差模塊的"直連"路徑,"直連"實際分兩種形式:殘差模塊輸入和輸出特徵通道數不等時,採用1x1卷積的升維操作;殘差模塊輸入和輸出通道相等時,採用直連操作。
? basicblock: 一個基礎殘差模塊,即圖9左邊所示,由兩組3x3卷積組成的路徑和一條"直連"路徑組成。
? layer_warp: 一組殘差模塊,由若干個殘差模塊堆積而成。每組中第一個殘差模塊滑動窗口大小與其他可以不同,以用來減少特徵圖在垂直和水平方向的大小。
def conv_bn_layer(input,
ch_out,
filter_size,
stride,
padding,
act="relu",
bias_attr=False):
tmp= fluid.layers.conv2d(
input=input,
filter_size=filter_size,
num_filters=ch_out,
stride=stride,
padding=padding,
act=None,
bias_attr=bias_attr)
return fluid.layers.batch_norm(input=tmp, act=act)
def shortcut(input, ch_in, ch_out, stride):
if ch_in!= ch_out:
return conv_bn_layer(input, ch_out, 1, stride, 0, None)
else:
return input
def basicblock(input, ch_in, ch_out, stride):
tmp= conv_bn_layer(input, ch_out, 3, stride, 1)
tmp= conv_bn_layer(tmp, ch_out, 3, 1, 1, act=None, bias_attr=True)
short= shortcut(input, ch_in, ch_out, stride)
return fluid.layers.elementwise_add(x=tmp, y=short, act="relu")
def layer_warp(block_func, input, ch_in, ch_out, count, stride):
tmp= block_func(input, ch_in, ch_out, stride)
for iin range(1, count):
tmp= block_func(tmp, ch_out, ch_out, 1)
return tmp
resnet_cifar10的連接結構主要有以下幾個過程。
底層輸入連接一層conv_bn_layer,即帶BN的卷積層。
然後連接3組殘差模塊即下面配置3組layer_warp,每組採用圖10 左邊殘差模塊組成。
最後對網路做均值池化並返回該層。
注意:除第一層卷積層和最後一層全連接層之外,要求三組layer_warp總的含參層數能夠被6整除,即resnet_cifar10的depth 要滿足(depth - 2) % 6 = 0
def resnet_cifar10(ipt, depth=32):
# depth should be one of 20, 32, 44, 56, 110, 1202
assert (depth- 2) % 6== 0
n= (depth- 2) // 6
nStages= {16, 64, 128}
conv1= conv_bn_layer(ipt, ch_out=16, filter_size=3, stride=1, padding=1)
res1= layer_warp(basicblock, conv1, 16, 16, n, 1)
res2= layer_warp(basicblock, res1, 16, 32, n, 2)
res3= layer_warp(basicblock, res2, 32, 64, n, 2)
pool= fluid.layers.pool2d(
input=res3, pool_size=8, pool_type="avg", pool_stride=1)
predict= fluid.layers.fc(input=pool, size=10, act="softmax")
return predict
4、Infererence配置
網路輸入定義為data_layer(數據層),在圖像分類中即為圖像像素信息。CIFRAR10是RGB 3通道32x32大小的彩色圖,因此輸入數據大小為3072(3x32x32)。
def inference_network():
# The image is 32 * 32 with RGB representation.
data_shape = [3, 32, 32]
images = fluid.layers.data(name="pixel", shape=data_shape, dtype="float32")
predict = resnet_cifar10(images, 32)
# predict = vgg_bn_drop(images) # un-comment to use vgg net
return predict
5、Train 配置
然後我們需要設置訓練程序train_network。它首先從推理程序中進行預測。在訓練期間,它將從預測中計算avg_cost。在有監督訓練中需要輸入圖像對應的類別信息,同樣通過fluid.layers.data來定義。訓練中採用多類交叉熵作為損失函數,並作為網路的輸出,預測階段定義網路的輸出為分類器得到的概率信息。
注意:訓練程序應該返回一個數組,第一個返回參數必須是avg_cost。訓練器使用它來計算梯度。
def train_network(predict):
label = fluid.layers.data(name="label", shape=[1], dtype="int64")
cost = fluid.layers.cross_entropy(input=predict, label=label)
avg_cost = fluid.layers.mean(cost)
accuracy = fluid.layers.accuracy(input=predict, label=label)
return [avg_cost, accuracy]
6、Optimizer 配置
在下面的Adam optimizer,learning_rate是學習率,與網路的訓練收斂速度有關係。
def optimizer_program():
return fluid.optimizer.Adam(learning_rate=0.001)
7、訓練模型
-1)Data Feeders 配置
cifar.train10()每次產生一條樣本,在完成shuffle和batch之後,作為訓練的輸入。
# Each batch will yield 128 images
BATCH_SIZE= 128
# Reader for training
train_reader = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.cifar.train10(), buf_size=128 * 100),
batch_size=BATCH_SIZE)
# Reader for testing. A separated data set for testing.
test_reader = paddle.batch(
paddle.dataset.cifar.test10(), batch_size=BATCH_SIZE)
-2)Trainer 程序的實現
我們需要為訓練過程制定一個main_program, 同樣的,還需要為測試程序配置一個test_program。定義訓練的place,並使用先前定義的優化器。
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
feed_order = ["pixel", "label"]
main_program = fluid.default_main_program()
star_program = fluid.default_startup_program()
predict = inference_network()
avg_cost, acc = train_network(predict)
# Test program
test_program = main_program.clone(for_test=True)
optimizer = optimizer_program()
optimizer.minimize(avg_cost)
exe = fluid.Executor(place)
EPOCH_NUM = 1
# For training test cost
def train_test(program, reader):
count = 0
feed_var_list = [
program.global_block().var(var_name) for var_name in feed_order
]
feeder_test = fluid.DataFeeder(feed_list=feed_var_list, place=place)
test_exe = fluid.Executor(place)
accumulated = len([avg_cost, acc]) * [0]
for tid, test_data in enumerate(reader()):
avg_cost_np = test_exe.run(
program=program,
feed=feeder_test.feed(test_data),
fetch_list=[avg_cost, acc])
accumulated = [
x[0] + x[1][0] for x in zip(accumulated, avg_cost_np)
]
count += 1
return [x / count for x in accumulated]
-3)訓練主循環以及過程輸出
在接下來的主訓練循環中,我們將通過輸出來來觀察訓練過程,或進行測試等。
# main train loop.
def train_loop():
feed_var_list_loop = [
main_program.global_block().var(var_name) for var_name in feed_order
]
feeder = fluid.DataFeeder(feed_list=feed_var_list_loop, place=place)
exe.run(star_program)
step = 0
for pass_id in range(EPOCH_NUM):
for step_id, data_train in enumerate(train_reader()):
avg_loss_value = exe.run(
main_program,
feed=feeder.feed(data_train),
fetch_list=[avg_cost, acc])
if step_id % 100 == 0:
print("
Pass %d, Batch %d, Cost %f, Acc %f" % (
step_id, pass_id, avg_loss_value[0], avg_loss_value[1]))
else:
sys.stdout.write(".")
sys.stdout.flush()
step += 1
avg_cost_test, accuracy_test = train_test(
test_program, reader=test_reader)
print("
Test with Pass {0}, Loss {1:2.2}, Acc {2:2.2}".format(
pass_id, avg_cost_test, accuracy_test))
if params_dirname is not None:
fluid.io.save_inference_model(params_dirname, ["pixel"],
[predict], exe)
train_loop()
-4)訓練
通過trainer_loop函數訓練, 這裡我們只進行了2個Epoch, 一般我們在實際應用上會執行上百個以上Epoch
注意:CPU,每個Epoch 將花費大約15~20分鐘。這部分可能需要一段時間。請隨意修改代碼,在GPU上運行測試,以提高訓練速度。
train_loop()
一輪訓練log示例如下所示,經過1個pass,訓練集上平均Accuracy 為0.59 ,測試集上平均Accuracy 為0.6 。
Pass 0, Batch 0, Cost 3.869598, Acc 0.164062
...................................................................................................
Pass 100, Batch 0, Cost 1.481038, Acc 0.460938
...................................................................................................
Pass 200, Batch 0, Cost 1.340323, Acc 0.523438
...................................................................................................
Pass 300, Batch 0, Cost 1.223424, Acc 0.593750
..........................................................................................
Test with Pass 0, Loss 1.1, Acc 0.6
圖13是訓練的分類錯誤率曲線圖,運行到第200個pass後基本收斂,最終得到測試集上分類錯誤率為8.54%。
圖13. CIFAR10數據集上VGG模型的分類錯誤率
應用模型
可以使用訓練好的模型對圖片進行分類,下面程序展示了如何載入已經訓練好的網路和參數進行推斷。
1、生成預測輸入數據
dog.png是一張小狗的圖片. 我們將它轉換成numpy數組以滿足feeder的格式.
from PIL import Image
def load_image(infer_file):
im = Image.open(infer_file)
im = im.resize((32, 32), Image.ANTIALIAS)
im = numpy.array(im).astype(numpy.float32)
# The storage order of the loaded image is W(width),
# H(height), C(channel). PaddlePaddle requires
# the CHW order, so transpose them.
im = im.transpose((2, 0, 1)) # CHW
im = im / 255.0
# Add one dimension to mimic the list format.
im = numpy.expand_dims(im, axis=0)
return im
cur_dir = os.path.dirname(os.path.realpath(__file__))
img = load_image(cur_dir + "/image/dog.png")
2、Inferencer 配置和預測
與訓練過程類似,inferencer需要構建相應的過程。我們從params_dirname載入網路和經過訓練的參數。我們可以簡單地插入前面定義的推理程序。現在我們準備做預測。
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
exe = fluid.Executor(place)
inference_scope = fluid.core.Scope()
with fluid.scope_guard(inference_scope):
# Use fluid.io.load_inference_model to obtain the inference program desc,
# the feed_target_names (the names of variables that will be feeded
# data using feed operators), and the fetch_targets (variables that
# we want to obtain data from using fetch operators).
[inference_program, feed_target_names,
fetch_targets] = fluid.io.load_inference_model(params_dirname, exe)
# The input"s dimension of conv should be 4-D or 5-D.
# Use inference_transpiler to speedup
inference_transpiler_program = inference_program.clone()
t = fluid.transpiler.InferenceTranspiler()
t.transpile(inference_transpiler_program, place)
# Construct feed as a dictionary of {feed_target_name: feed_target_data}
# and results will contain a list of data corresponding to fetch_targets.
results = exe.run(
inference_program,
feed={feed_target_names[0]: img},
fetch_list=fetch_targets)
transpiler_results = exe.run(
inference_transpiler_program,
feed={feed_target_names[0]: img},
fetch_list=fetch_targets)
assert len(results[0]) == len(transpiler_results[0])
for i in range(len(results[0])):
numpy.testing.assert_almost_equal(
results[0][i], transpiler_results[0][i], decimal=5)
# infer label
label_list = [
"airplane", "automobile", "bird", "cat", "deer", "dog", "frog",
"horse", "ship", "truck"
]
print("infer results: %s" % label_list[numpy.argmax(results[0])])
總結
傳統圖像分類方法由多個階段構成,框架較為複雜,而端到端的CNN模型結構可一步到位,而且大幅度提升了分類準確率。本文我們首先介紹VGG、ResNet兩個經典的模型;然後基於CIFAR10數據集,介紹如何使用PaddlePaddle配置和訓練CNN模型;最後介紹如何使用PaddlePaddle的API介面對圖片進行預測和特徵提取。對於其他數據集比如ImageNet,配置和訓練流程是同樣的。請參照Github
https://github.com/PaddlePaddle/models/blob/develop/PaddleCV/image_classification/README_cn.md。
參考文獻
[1] G.E. Hinton, N. Srivastava, A. Krizhevsky, I. Sutskever, and R.R. Salakhutdinov. Improving neural networks by preventing co-adaptation of feature detectors. arXiv preprint arXiv:1207.0580, 2012.
[2] K. Chatfield, K. Simonyan, A. Vedaldi, A. Zisserman. Return of the Devil in the Details: Delving Deep into Convolutional Nets. BMVC, 2014。
[3] K. He, X. Zhang, S. Ren, J. Sun. Deep Residual Learning for Image Recognition. CVPR 2016.
[4] He, K., Zhang, X., Ren, S., and Sun, J. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. ArXiv e-prints, February 2015.
[5] http://deeplearning.net/tutorial/lenet.html
[6] https://www.cs.toronto.edu/~kriz/cifar.html
[7] http://cs231n.github.io/classification/
TAG:機器之心 |