1. 程式人生 > >深度學習與計算機視覺(PB-12)-ResNet

深度學習與計算機視覺(PB-12)-ResNet

系列學習:

在上一章中,我們討論了GoogLeNet網路結構和Inception模組,這節中,我們將討論由一個新的微結構模組組成的網路結構,即由residual微結構組成的網路結構——ResNet。

ResNet網路由residual模組串聯而成,在原論文中,我們發現作者訓練的ResNet網路深度達到了先前認為不可能的深度。在2014年,我們認為VGG16和VGG19網路結構已經非常深了。然而,通過ResNet網路結構,我們發現可以成功在ImageNet資料集上訓練超過100層的網路和在CIFAR-10資料集上訓練超過1000層的網路。

從論文《Identity Mappings in Deep Residual Networks》中,可知只有使用更高階的權值初始化演算法(如Xavier等)以及identity mapping(恆等對映)才能實現這些深度,我們將在本章後面討論相關內容。我們知道CNN能夠提取low/mid/high-level的特徵,網路的層數越多,意味著能夠提取到不同level的特徵越豐富。並且,越深的網路提取的特徵越抽象,越具有語義資訊。非常深的ResNet網路在ILSVRC 2015年所有三項挑戰(分類、檢測和定位)中獲得了第一名。

在本文中,首先,我們將討論ResNet網路結構、residual模組,以及residual模組的擴充套件。之後,我們將在CIFAR-10資料集和tiny imagenet資料集上訓練ResNet網路結構。

ResNet網路和residual模組

He等人在2015年的論文《Deep Residual Learning for Image Recognition》提出了ResNet網路結構,並證明了使用標準的SGD優化演算法和合理的初始化函式可以訓練非常深的網路。從論文中,我們可以看到ResNet網路深度可以達到50-100層以上(甚至1000層),主要依賴於residual模組。

另外,從論文中,可以發現ResNet網路結構很少使用pooling層,這跟以往我們搭建的卷積神經網路不同。ResNet網路並不完全依賴於max pooling層來降低feature map的大小。相反,使用步長大於1的卷積來降低輸出的feature map大小。事實上,在搭建網路結構時,只有兩種情況可能會使用pooling層:

  • 為了降低feature map大小,在搭建網路主體部分使用max pooling。
  • 像GoogLeNet一樣,用average pooling層代替全連線層。

嚴格地說,ResNet網路只有一個max pooling層——所有降維效果都是由卷積層完成。

我們將回顧原始的residual模組,以及用於訓練更深層次網路的bottleneck residual模組。接著,我們將討論He等人在2016年發表的《Identity Mappings in Deep Residual Networks》論文中對原始residual模組的擴充套件,從而進一步提高分類準確率。最後,我們將基於Keras框架從頭到尾實現ResNet網路結構,並在CIFAR-10資料集和tiny imagenet資料集上進行訓練。

residual 和 Bottlenecks residual

He等人在2015年提出的residual模組依賴於identity mappings。如圖12.1(左)所示,residual模組主要有兩個分支:

  • 右邊分支(“彎彎的部分”)輸出的是原始輸入
  • 左邊分支輸出的是對原始輸入經過一系列卷積變換

最後將兩分支輸出進行相加得到整個residual模組的輸出。從圖12.1可以看到residual模組只有兩個分支,而我們上節提到的GoogLeNet的Inception模組有四個分支。所以,相對而言,residual模組非常簡單。

圖12.1 左:原始的residual模組 右:bottleneck residual模組

從圖12.1(左),可知He等人將原始輸入直接與經過CONV、RELU和BN層計算的輸出相加,我們稱這個加法為identity mapping(標識對映)。注意,我們並不是像前面幾章所做的那樣,沿著通道維度連線,而是我們將兩個分支的輸出按照1+1=2的加法模式。

傳統的神經網路層可以看作是學習一個函式y = f(x),而residual模組可以看做是學習一個函式y=f(x)+id(x)=f(x)+x,其中id(x)是恆等對映函式。我們考慮y作為residual模組要擬合的基礎對映,x表示residual模組的輸入,假設多個非線性層可以漸進地近似複雜函式,它等價於假設它們可以漸進地近似殘差函式,即f(x) = y-x(假設輸入輸出是相同的維度)。因此,我們明確讓這些層近似引數函式f(x) = y-x,而不是期望堆疊層近似y(圖12.1左圖的左分支)。因此原始函式變成了f(x)+x。儘管兩種形式應該都能漸進地近似要求的函式(假設),但學習的難易程度可能是不同的。

此外,由於每個residual模組都包含輸入,所以學習率越大,網路訓練越快(一般來說,學習率越大,神經網路學習速度越快,如果學習率太小,網路很可能會陷入區域性最優,但是如果太大,超過了極值,損失就會停止下降,在某一位置反覆震盪)。一般訓練ResNet網路的初始學習率為1e-1,但是對於大多數網路結構,比如AlexNet或VGGNet,這麼高的學習率,網路幾乎不可能會收斂。由於ResNet網路的resident模組包含identity mapping,所以高學習率是可行的。

此外,He等人也對原始residual模組進行了擴充套件——bottleneck(如圖12.1右),從圖12.1右,我們可以發現右分支的identity mapping沒有發生變化,而左分支的conv層進行了更新:

  • 使用三個CONV層,而不是兩個。
  • 第一層和最後一層是1x1卷積。
  • 前兩個CONV層的filter的數量等於最後一個CONV層的filter的數量的1/4。

bottleneck的結構如圖12.2所示,這裡我們列了兩個residual模組,其中一個residual模組輸出直接輸入到下一個residual模組。

image

圖12.2 bottleneck案例

第一個residual模組接收大小為MxNx64的feature map(這個示例的寬度和高度是任意的),且三個CONV層的filter個數分別為32、32和128。最後residual模組輸出的feature map大小為MxNx128,然後將其傳遞到第二個residual模組中。

在第二個residual模組中,三個CONV層的filter個數分別為32、32和128。需要注意的是 32 < 128,意味著通過1x1和3x3CONV層,我們降低了feature map的大小。

最後一個1x1CONV層的filter個數是前兩個CONV層的filter個數的4倍,從而再次增加了feature map的個數,這就是為什麼我們把對residual模組的改進稱為“bottleneck”——其意思就是輸入輸出維度差距較大,就像一個瓶頸一樣,上窄下寬亦或上寬下窄。在構建自定義residual模組時,通常會提供一些虛擬碼,比如residual_module(K=128),這意味著residual模組中最後一個CONV層的filter個數為128個,從而可以推匯出前兩個conv層的filter個數為128/4 = 32個。這種表示法通常更容易使用。

在構建ResNet網路程式碼之前,我們再來認識下identity mapping。

identity mapping

ResNet網路中提出的Residual block之所以成功,原因有兩點:

  • 第一,是它的shortcut connection增加了它的資訊流動,
  • 第二,就是它認為對於一個堆疊的非線性層,那麼它最優的情況就是讓它成為一個恆等對映,但是shortcut connection的存在恰好使得它能夠更加容易的變成一個identity mapping。
    比如:
    image

下面那行的網路其實就是在上面那行網路的基礎上新疊加了一層,而新疊加上那層的權重weight,如果能夠學習成為一個恆等的矩陣I,那麼其實上下兩個網路是等價的,那麼也就是說如果繼續堆疊的層如果能夠學到一個恆等矩陣,那麼經過堆疊的網路是不會比原始網路的效能差的,也就是說,如果能夠很容易的學到一個恆等對映,那麼更深層的網路也就更容易產生更好的效能。這是ResNet所提出的根源,也是本文所強調的重點。

對於一個網路中的一個卷積層f(x,W),W是卷積層的權重,如果要使得這個卷積層是一個恆等對映,即f(x,W)=x,那麼W就應該是一個恆等對映I,但是當模型的網路變深時,要使得W=I 就不那麼容易。對於ResNet的每一個Residual Block,要使得它為一個恆等對映,即f(x,W)+x=x,就只要使得W=0即可,而學習一個全0的矩陣比學習一個恆等矩陣要容易的多,這就是ResNet在層數達到幾百上千層時,依然不存在優化難題的原因。

Residual模組的擴充套件

2016年,He等人發表了第二篇關於residual模組的論文《Identity Mappings in Deep Residual Networks》。該論文從多個角度詳細介紹了residual模組內部的卷積、啟用和BN層的位置排序問題,包括從理論和實踐角度。首先,我們來看看bottleneck residual模組,如圖12.3(左)所示:

image

圖12.3 左: bottleneck residual 右:pre-activation residual
原bottleneck residual模組對輸入層做兩個分支,由圖12.3(左)可知,右邊分支對輸入層應用一系列(CONV = > BN = > ReLU)x2 = > CONV = > BN變換,左分支直接輸出輸入,然後將兩輸出進行相加並新增RELU啟用。然而,在He等2016年的研究中,發現有一種更優的層序能夠獲得更高的精度——這種方法被稱為預啟用(pre-activation)。

在pre-activation版本的residual模組中,如圖12.3(右)所示,我們去掉模組底部的ReLU啟用層,並且把residual mapping中的BN層和RELU啟用層放在了CONV層之前,從圖12.3左右對比,很明顯看到這一變換。

因此,我們不再從CONV層開始,而是應用一系列(BN => RELU => CONV) x 3。residual模組的輸出是兩個分支的輸出進行加法得到,它隨後被輸入到網路中的下一個residual模組中(因為residual模組是疊加在一起的)。

作者將ReLU和BN看做是權重層的”pre-activation”,基於這個觀點,作者設計瞭如圖12.3(右)所示的新的residual模組,相比原始的residual模組,新結構更加易於訓練且取得了更好的訓練結果。

實現 ResNet

上面,我們討論了ResNet網路結構相關內容,接下來,我們將基於Keras框架實現ResNet網路。注意,我們將實現最新版本的residual模組,包括bottlenecks和pre-activations。在pyimagesearch專案中的nn.conv子目錄中建立一個resnet.py(若存在,則進行更新),整體專案目錄結構如下:

--- pyimagesearch
| |--- __init__.py
| |--- callbacks
| |--- io
| |--- nn
| | |--- __init__.py
| | |--- conv
| | | |--- __init__.py
| | | |--- alexnet.py
| | | |--- deepergooglenet.py
| | | |--- lenet.py
| | | |--- minigooglenet.py
| | | |--- minivggnet.py
| | | |--- fcheadnet.py
| | | |--- resnet.py
| | | |--- shallownet.py
| |--- preprocessing
| |--- utils

開啟resnet.py,並寫入以下程式碼:

#encoding:utf-8
# 載入所需模組
from keras.layers import BatchNormalization
from keras.layers import Conv2D
from keras.layers import AveragePooling2D
from keras.layers import MaxPooling2D
from keras.layers import ZeroPadding2D
from keras.layers import Activation
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import Input
from keras.models import Model
from keras.layers import add
from keras.regularizers import l2
from keras import backend as K

其中,add函式主要是將residual模組的兩個分支輸出相加,l2函式是正則項,在訓練非常深的網路時,正則化非常重要,因為網路越深,越容易發生過擬合問題。

接下來,構建residual_module:

class ResNet:
    @staticmethod
    def residual_module(data,K,stride,chanDim,red = False,reg = 0.0001,bnEps=2e-5,bnMom=0.9):

注意:我們基於keras框架實現的ResNet網路將類似於He的caffe版本和wei wu的mxnet版本,因此,引數具體的取值主要與兩個版本保持一致。

其中:

  • data: residual模組的輸入,也就是上一層的輸出
  • K:filter的個數,注意: K值是對應bottleneck中最後一個conv層的filters個數,前面兩個conv層的filters個數為K/4,主要參考He的設定。
  • stride:步長,我們將設定這個引數來達到降低feature map大小的效果,而不是使用max pooling。
  • chanDim:通道位置
    red:(reduce的縮寫),bool型別,是否降低feature map的大小,這裡主要是表明不是所有的residual模組都進行降維。
  • reg:正則化係數,這裡會對所有的conv層使用l2正則。
  • bnEps:bn引數,主要預防標準化時,分母為0,在keras模組中,預設值為0.001,這裡我們將預設設定為2e-5,主要降低該值得影響。
  • bnMom:bn引數,動態均值的動量。在keras中預設值為0.99,而he和wei wu版本中都設定為0.9.

接下來,定義residual模組的主體:

# resnet模組中shortcut分支
# 恆等對映
shortcut = data
# bottleneck第一個block是1x1卷積 
bn1 = BatchNormalization(axis = chanDim,epsilon=bnEps,momentum = bnMom)(data)
act1 = Activation('relu')(bn1)
conv1 = Conv2D(int(K*0.25),(1,1),use_bias = False,kernel_regularizer=l2(reg))(act1)

shortcut對應residual模組中的identity mapping,之後,我們會將shortcut與residual mapping輸出相加。

residual mapping的第一個pre-activation由BN、relu和conv組成,先BN層,然後是RELU啟用層,最後是1x1conv層,需要注意的是,1x1卷積層的filters個數為K/4,且use_bais=False表明在conv層中不使用偏置項,根據He的研究,BN層的作用相當於bias,若conv層後緊接著bn層,則conv層中可以不使用bias。

構建residual mapping中的第二個block:

# ResNet第二個block是3x3卷積
bn2 = BatchNormalization(axis = chanDim,epsilon=bnEps,momentum=bnMom)(conv1)
act2 = Activation('relu')(bn2)
conv2 = Conv2D(int(K*0.25),(3,3),strides = stride,padding='same',use_bias=False,kernel_regularizer=l2(reg))(act2)

其中,filters同樣為K/4,而大小變成了3x3.

構建residual mapping中的最後一個block:

# ResNet第三個block是1x1卷積
bn3 = BatchNormalization(axis = chanDim,epsilon=bnEps,momentum=bnMom)(conv2)
act3 = Activation('relu')(bn3)
conv3 = Conv2D(K,(1,1),use_bias=False,kernel_regularizer=l2(reg))(act3)

稍微注意下,最後一個block的conv層的filters個數為K,大小為1x1.

是否需要降低feature map的大小:

#如果我們想搞降低feature map的個數,可以使用1x1卷積
if red:
    shortcut = Conv2D(K,(1,1),strides=stride,use_bias=False,kernel_regularizer=l2(reg))(act1)

如果我們需要減小feature map的個數,則我們可以對shortcut使用步長大於1的conv層。

將residual mapping中conv3的輸出與shortcut相加,作為residual_module的輸出:

# 兩個分支輸出相加
x = add([conv3,shortcut])
return x

接下來,我們將使用residual_module搭建整個resnet網路結構:

@staticmethod
def build(width,height,depth,classes,stage,filters,reg = 0.0001,bnEps=2e-5,bnMom=0.9,dataset='cifar'):

其中:

  • width:輸入影象的寬度
  • height:輸入影象的高度
  • depth:通道數,即深度
  • classes:類別個數
  • stage:list形式,列表中的每一值代表我們需要堆疊的residual模組的個數(每一個residual模組的filters個數相同)
  • filters:list形式,列表中的每一個值代表conv層中filter的個數,注意:第一個值為第一個conv層的filter個數。
  • reg:l2正則化係數,預設值為0.0001
  • bnEps:BN引數,防止標準化時分母為0,預設值為2e-5
  • bnMom:動態均值的動量,預設值為0.9
  • dataset:訓練資料的型別,預設是在cifar資料集上訓練

需要注意的是:stage和filters值的形式,兩者都是list形式,比如假設stage =[3,4,6]和filters=[64,128,256,512]——注意stage和filters值的個數,stage列表與filters列表後三個值是一一對應。filters列表中第一個數值表示單獨conv層的filter個數,而不是residual模組中conv層的filter個數,因此第一個filter值為64表明,ResNet網路的第一個conv層的filter個數為64。然後,我們把三個residual模組堆疊在一起——每個residual模組的filters引數K=128,降低了feature map的大小。接著,我們把四個residual模組疊加在一起——每個residual模組的引數K = 256,經過四個residual模組之後,我們再一次降低了feature map的大小。最後,我們把六個residual模組堆疊在一起——每個residual模組的引數K = 512。

使用列表來儲存stage和filters數值,這樣方便我們構建更深的網路,而不會增加程式碼量。

接下來,定義input的shape:

# 初始化輸入shape
inputShape = (height,width,depth)
chanDim = -1
if K.image_data_format() == "channels_first":
    inputShape = (depth,height,width)
    chanDim = 1

ResNet網路的input層:

# 輸入層
inputs = Input(shape=inputShape)
# 對輸入進行BN
x = BatchNormalization(axis = chanDim,epsilon = bnEps,momentum = bnMom)(inputs)
# 使用資料型別
if dataset == 'cifar':
    # 卷積層
    x = Conv2D(filters[0],(3,3),use_bias = False,padding='same',kernel_regularizer = l2(reg))(x)

ResNet的第一層為BN層,這與我們之前看到的網路結構有點不同(之前網路的第一層一般都是CONV層)。以往我們構建網路結構時,需要對輸入資料進行標準化,一般的做法是減去均值,作者在這裡再加了一層BN層,主要是為了資料更加標準。事實上,直接對輸入資料進行BN運算有時可以不用對輸入資料進行零均值處理。

BN層之後,緊接著是一個CONV層,filter的個數為filters列表的第一個值(注:所有的conv層的引數都在filters列表中),大小為3x3。需要注意的是,該conv層是針對cifar-10資料集,之後,我們會在tiny imagenet資料集上也訓練ResNet網路,即dataset=’tiny_imagenet’,而對於tiny imagenet資料集,由於tiny imagenet資料的圖片shape比較大,因此在堆疊residual模組之前,我們會對輸入資料進行一系列的卷積、bn和max pooling等操作。

接下來,我們開始堆疊residual模組:

# 遍歷階段個數
for i in range(0,len(stage)):
    # 初始化步長
    # 減少feature map的個數
    stride = (1,1) if i==0 else (2,2)
    x = ResNet.residual_module(x,filters[i+1],stride,chanDim,red=True,bnEps = bnEps,bnMom=bnMom)
    # 每階段的residual模組個數
    for j in range(0,stage[i] -1):
        x = ResNet.residual_module(x,filters[i+1],(1,1),chanDim,bnEps=bnEps,bnMom=bnMom)

注意:stage列表每一個數值表示多少個residual模組堆疊在一起。從整個網路結構中,我們可知ResNet網路儘可能地減少pooling層的使用,而依靠conv層來降低feature map的大小。

若不依賴pooling層減小feature map大小,則我們必須設定conv的步長,但是,在第一個stage的所有residual模組的conv層的步長為1,表明不進行下采樣處理,而之後的stage的所有conv層的步長為2,此時可以達到減小feature map大小的作用。

經過一系列的residual模組之後,特徵影象的大小減少到8x8xclases,類似於GoogLeNet網路,為了避免使用全連線層,我們將使用average pooling層將特徵影象的大小縮小到1x1xclasses:

# BN => ACT => POOL
x = BatchNormalization(axis=chanDim,epsilon = bnEps,momentum = bnMom)(x)
x =Activation('relu')(x)
x = AveragePooling2D((8,8))(x)

最後,構建softmax層返回預測概率:

# 分類器
x = Flatten()(x)
x = Dense(classes,kernel_regularizer=l2(reg))(x)
x = Activation('softmax')(x)
# 建立模型
model = Model(inputs,x,name='resnet')
return model

以上,我們搭建好了ResNet網路結構,接下來我們將在CIFAR-10資料集和tiny imagenet資料集上進行訓練。

ResNet on CIFAR-10

首先,我們將在CIFAR-10資料集上訓練ResNet網路結構,並且我們將復現he的實驗結果。

CIFAR-10資料:ctrl + c 方法

當我們一開始訓練一個不太熟悉的網路結構或者一個未使用過的資料集時,一般使用ctrl+c的方法進行訓練網路。我們可以根據訓練過程結果對學習率進行調整,當我們完全不確定一個網路結構或者資料集需要訓練多長時間才能獲得合理的精度時,這個方法尤其有用。

從之前的CIFAR-10資料實驗中,我們覺得對cifar-10資料集訓練網路大概需要迭代60-100次左右。對於ResNet網路是需要更多還是更少?,一開始我們也無法確定,但是至少可以先從之前的實驗經驗獲得一個大概的次數,然後根據實驗結果,在決定是多還是少。因此,我們將根據之前的cifar-10資料實驗中,確定超引數的變化範圍。

首先,新建一個名為resnet_cifar10.py檔案,並寫入以下程式碼:

#encoding:utf-8
# 記載所需模組
import matplotlib
matplotlib.use("Agg")
from sklearn.preprocessing import LabelBinarizer
from pyimagesearch.nn.conv import resnet
from pyimagesearch.callbacks import epochcheckpoint as EPO
from pyimagesearch.callbacks import trainingmonitor as TM
from keras.preprocessing.image import ImageDataGenerator
from keras.datasets import cifar10
from keras.optimizers import SGD
from keras.models import load_model
import keras.backend as K
import numpy as np
import argparse

其中,我們載入了EpochCheckpoint類,以便在訓練過程中將ResNet權值序列化到磁碟,從而可以從特定的checkpoint停止並重新開始訓練,第一個實驗中,我們將使用SGD進行訓練。

解析命令列引數:

# 解析命令列引數
ap = argparse.ArgumentParser()
ap.add_argument('-c','--checkpoints',required=True,help='path to output checkpoint directory')
ap.add_argument('-m','--model',type=str,help='path to *specific* model checkpoint to load')
ap.add_argument('-s','--start_epoch',type=int,default =0,help='epoch to restart training as ')
args = vars(ap.parse_args())

其中:

  • checkpoints:儲存checkpoint模型路徑
  • model:模型儲存路徑
  • start_epoch:重新開始訓練checkpoint模型

載入cifar-10資料集——已經劃分好了train和test,並進行零均值預處理和標籤編碼化:

# 載入train和test資料集
print('[INFO] loading CIFAR-10 data...')
((trainX,trainY),(testX,testY)) = cifar10.load_data()
# 轉化為float
trainX = trainX.astype("float")
testX = testX.astype("float")
# 計算RGB通道均值
mean = np.mean(trainX,axis =0)
# 零均值化
trainX -= mean
testX -= mean

# 標籤編碼處理
lb = LabelBinarizer()
trainY = lb.fit_transform(trainY)
testY = lb.fit_transform(testY)

與之前的實驗一樣,為了提高精度和防止過擬合,我們對train資料進行資料增強處理:

# 資料增強
aug = ImageDataGenerator(width_shift_range = 0.1,
                         height_shift_range = 0.1,
                         horizontal_flip = True,
                         fill_mode='nearest')

如果我們第一次訓練網路,則需要初始化網路結構:

# 若未指定checkpoints模型,則直接初始化模型
if args['model'] is None:
    print("[INFO] compiling model...")
    opt = SGD(lr=1e-1)
    model = resnet.ResNet.build(32,32,3,10,(9,9,9),(64,64,128,256),reg=0.0005)
    model.compile(loss='categorical_crossentropy',optimizer=opt,metrics = ['accuracy'])

可能你會注意到SGD的學習率為0.1,比之前的任何實驗的學習率都要高,這是由於在residual模組中存在identity mapping,使得我們可以使用大學習率。但是這個大學習率並不適合像VGG或AlexNet的網路結構。

在初始化ResNet模型時,輸入影象的大小為32x32x3,cifar-10有10種不同類別,因此classes=10,需要特別注意的是stage和filters列表。stage=(9,9,9),也就是說我們將學習三個階段,每個階段由9個residual模組堆疊而成。在每一個階段中,我們使用特定的residual模組來降低feature map的大小。

filters=(64、64、128、256)是CONV層的filter個數。第一個CONV層(在residual模組之前)由K = 64個filter組成。而64、128和256分別對應於每個stage中conv層的filter個數,即前9個residual模組的引數K = 64。第二組的9個residual模組的引數K = 128,最後一組的9個residual模組的引數K = 256。

如果,給定checkpoint,我們將從特定的模型開始訓練,並更新學習率:

# 否則從磁碟中載入checkpoints模型
else:
    print("[INFO] loading {}...".format(args['model']))
    model = load_model(args['model'])
    # 更新學習率
    print("[INFO] old learning rate: {}".format(K.get_value(model.optimizer.lr)))
    K.set_value(model.optimizer.lr,1e-5)
    print("[INFO] new learning rate: {}".format(K.get_value(model.optimizer.lr)))

回撥函式列表:

  • (1)檢每5次epoch,儲存一次當前ResNet模型
  • (2)監控整個訓練過程:
# 回撥函式列表
callbacks = [
    # checkpoint
    EPO.EpochCheckpoint(args['checkpoints'],every = 5,startAt = args['start_epoch']),
    # 監控訓練過程
    TM.TrainingMonitor("output/resnet56_cifar10.png",
                       jsonPath="output/resnet56_cifar10.json",
                       startAt = args['start_epoch'])
]

訓練網路

# 訓練網路
print("[INFO] training network.....")
model.fit_generator(
    aug.flow(trainX,trainY,batch_size=128),
    validation_data = (testX,testY),
    steps_per_epoch = len(trainX) // 128,
    epochs = 10,
    callbacks = callbacks,
    verbose =1