一般來說, 影像分類透過手工提取特徵或特徵學習方法對整個影像進行全部描述,然後使用分類器判別物體類別,因此如何提取影像的特徵至關重要。基於 深度學習的 影像分類方法,可以透過有監督或無監督的方式學習層次化的特徵描述,從而取代了手工設計或選擇影像特徵的工作。
深度學習模型中的 卷積神經網路(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/