MXNet/Gluon第四課:BatchNorm,更深的卷積神經網路,圖像增強筆記
堅持到第四課了。。。
首先,先講一個batchnorm中非常重要的概念,那就是批量歸一化。關注過我的深度學習專欄的小夥伴應該對這個概念不陌生了,我在BatchNormalization學習筆記中對BN中的各個方面的細節都做了非常詳細的介紹,具體內容參考的是李宏毅的ppt,大家有興趣可以去我的這個專欄來看看。這節課就不從原理和概念的角度來闡述什麼的BN,以及BN的作用力,更多的是從代碼的角度來一步一步的實現batchnormalization。
批量歸一化:
關於批量歸一化的實現,其實要分兩種情況來考慮:
- 一種情況是針對傳統的全連接的神經網路
- 一種情況是針對帶二維卷積層的神經網路
因為通過前面教程的學習,我們知道,
- 傳統的全連接層的神經網路的參數是batch_size*feature的形式
- 二維卷積層的神經網路的參數是batch_size*channels*height*width
其中,
- 對於傳統的全連接的神經網路來說,我們要做的是對每一個批量進行歸一化。並且不能改變其最終的shape。
- 對於二維的卷積的神經網路來說,我們要做的是對每一個channel進行歸一化,並且不能改變其最終的shape。
下面的代碼實現了一個簡單的batchnormalization
def pure_batch_norm(X,gamma,beta,eps = 1e-5): assert len(X.shape) in (2,4) # fully connected neural network or Convolutional Neural Network if len(X.shape) == 2: # fully connected neural network for every feature # batch_size * feature mean = X.mean(axis = 0) variance = ((X-mean)**2).mean(axis = 0) else: # calculate mean and variance for every channel # batch_size * channel * height * width mean = X.mean(axis=(0,2,3),keepdims = True) variance = ((X-mean)**2).mean(axis=(0,2,3),keepdims = True) # batchnorm X_hat = (X-mean)/nd.sqrt(variance+eps) # scale and shift return gamma.reshape(mean.shape)*X_hat+beta.reshape(mean.shape)A = nd.arange(6).reshape((3,2))print (A)res = pure_batch_norm(A,gamma = nd.array([1,1]),beta = nd.array([0,0]))print (res)
通過print的輸出結果的對比,我們可以看出來:
[[ 0. 1.]
[ 2. 3.] [ 4. 5.]]<NDArray 3x2 @cpu(0)>[[-1.22474265 -1.22474265]
[ 0. 0. ] [ 1.22474265 1.22474265]]<NDArray 3x2 @cpu(0)>其確實被歸一化成為了一個新的X,並且其滿足均值為0,方差為1.這是對於傳統的全連接網路的測試。
接下來,對於二維的卷積網路,我們也試試這樣的操作。
B = nd.arange(18).reshape((1,2,3,3))print (B)res = pure_batch_norm(B,gamma = nd.array([1,1]),beta = nd.array([0,0]))print (res)
[[[[ 0. 1. 2.]
[ 3. 4. 5.]
[ 6. 7. 8.]]
[[ 9. 10. 11.]
[ 12. 13. 14.]
[ 15. 16. 17.]]]]
<NDArray 1x2x3x3 @cpu(0)>
[[[[-1.54919219 -1.1618942 -0.7745961 ]
[-0.38729805 0. 0.38729805]
[ 0.7745961 1.1618942 1.54919219]]
[[-1.54919219 -1.1618942 -0.7745961 ]
[-0.38729805 0. 0.38729805]
[ 0.7745961 1.1618942 1.54919219]]]]
<NDArray 1x2x3x3 @cpu(0)>
實際結果和預期相同,其實,在輸入進batchnorm中的gamma和beta都是神經網路通過學習得到的,這裡為了簡化,我們自己手動設置為了1和0。
批量歸一化層
前面講的都是在訓練的過程中,怎麼使用batch normalization來進行歸一化。但是,既然訓練的時候用了,那麼我們在測試的時候,也必須使用這種手段。這個時候,很多小夥伴們可能就不知道測試(inference)的時候該怎麼操作了,讓我們來分析下這個問題的難點:
- 如果僅僅在訓練的時候使用了batch normalization,而在測試的時候不使用batch normalization。那麼,可能在inference的時候,就不是那麼準確了。
- 但是,如果要用batch normalization的話,該使用哪裡的mean和variance呢?畢竟在做inference的時候,我們只有一個sample,並不像training的時候,有一個batch的數據,可以非常容易的求出來mean和variance
在實際的操作中,我們還是需要繼續使用批量歸一化的,我們只需要稍微加一些調整。我們可以把在training階段的mean和variance都記錄下來,然後在測試的時候,使用這些算出來的mean和variance就行了(具體實現,可以參考moving_mean和moving_variance)
def batch_norm(X,gamma,beta,is_training,moving_mean,moving_variance,eps=1e-5,moving_momentum = 0.9): assert len(X.shape) in (2,4) # fully connected layer # batch_size * feature if len(X.shape) == 2: # every samples mean and variance mean = X.mean(axis=0) variance = ((X-X.mean)**2).mean(axis=0) # 2D Conv2D: batch_size * channels * height * width else: mean = X.mean(axis = (0,2,3),keepdims = True) variance = ((X-X.mean)**2).mean(axis = (0,2,3),keepdims = True) # in order to adapt the broadcasting moving_mean = moving_mean.reshape(mean.shape) moving_variance = moving_variance.reshape(mean.shape) # batchnorm if is_training: # for training X_hat = (X-mean)/nd.sqrt(variance+eps) # update the global mean and variance moving_mean[:] = moving_momentum * moving_mean + (1.0 - moving_momentum) * mean moving_variance[:] = moving_momentum * moving_variance + ( 1.0 - moving_momentum) * variance else: # for test X_hat = (X-moving_mean)/nd.sqrt(moving_variance+eps) return gamma.reshape(mean.shape)*X_hat+beta.reshape(mean.shape)
一般,我們把batch normalization加入到卷積結束和非線性激勵之前來進行。
網路中的網路
我們還是從最最早的AlexNet來看,通過對於AlexNet的學習,我們知道了幾個重要的概念,對於我們今後進行網路的設計都提供了非常好的思路。
首先注意到:
- 卷積神經網路一般分為兩塊:一塊是由卷積層組成的,另外一塊是通過全連接層組成的
- 在AlexNet中,我們可以看到如何將卷積層和全連接層分別加寬和加深,最終得到了更深的卷積神經網路
- 其實,還有一種思路就是VGG中涉及到的,將數個卷積層和全連接層串聯起來構成一個block,然後將block通過stack出,構成更深的architecture
但是,細心的小夥伴們可能會發現。
- 卷積的輸入輸出都是一個4D的數據:batch_size*channels*height*width
- 全連接的輸入和輸出都是一個2D的數據:batch_size*feature
從4D->2D的轉換過程中,肯定會使得全連接層具有更多的參數,從而不論對於硬體移植還是訓練過程來說,都帶來了不可避免的麻煩。
NIN提出了一種很好的解決策略:
- 對通道層做全連接
- 像素之間共享權重
下面,我們通過代碼來實現下這個NIN的結構,它由一個正常的卷積層接上兩個kernel是1*1的卷積層構成,後面兩個1*1的卷積層充當了全連接層的作用。
下面,通過代碼來實現來,什麼是NIN
def mlpconv(channels,kernel_size,padding,strides = 1,max_pooling = True): out = nn.Sequential() out.add( nn.Conv2D(channels = channels, kernel_size = kernel_size, strides = strides, padding = padding, activation = relu), nn.Conv2D(channels = channels, kernel_size = 1, padding = 0, strides = 1, activation = relu), nn.Conv2D(channels = channels, kernel_size = 1, padding = 0, strides = 1, activation = relu) ) if max_pooling: out.add(nn.MaxPool2D(pool_size = 3,strides = 2)) return out blk = mlpconv(64,3,0)blk.initialize()x = nd.random.uniform(shape = (32,3,16,16))y = blk(x)print (y.shape)
通過列印的結果信息,可以得到從輸入的(32,3,16,16)變成了(32, 64, 6, 6)
其中batch_size的個數沒有發生變化,還是32,但是,channel的個數比以前變多了,從3變成了64個,height和width的尺寸都相比以前變小了很多。
除了使用了1*1的卷積層以外,NIN最後也是不使用全連接層的,而是使用一個通道數為輸出類別個數的mlpconv,然後再接一個global average pooling(全局平均池化層)來將每個通道里的數值平均成一個標量。
所以說,最終的網路結構就是這樣的:
net = nn.Sequential()with net.name_scope(): net.add( mlpconv(96,11,0,strides = 4), mlpconv(256,5,2), mlpconv(384,3,1), nn.Dropout(0.5), mlpconv(10,3,1,max_pooling = False), nn.GlobalAvgPool2D(), nn.Flatten() )
更深的卷積神經網路:GoogLeNet
googlenet是2014年的ImageNet世界冠軍,它的出現很大意義上是借鑒了NIN的工作和思路。
首先給出GoogLeNet的總的架構:
它其實是通過紅色框起來的Inception一步一步的通過stack的方式得到的,那麼,接下來,我們就來講講什麼Inception吧
定義Inception
可以看到,其中有多個四個並行卷積層的模塊,這個塊就叫做是Inception
讓我們通過代碼看看,如何定義上層所示的Inception模塊。
class Inception(nn.Block): def __init__(self, n1_1, n2_1, n2_3, n3_1, n3_5, n4_1, **kwargs): super(Inception, self).__init__(**kwargs) # path 1 self.p1_conv_1 = nn.Conv2D(n1_1, kernel_size=1,activation=relu) # path 2 self.p2_conv_1 = nn.Conv2D(n2_1, kernel_size=1,activation=relu) self.p2_conv_3 = nn.Conv2D(n2_3, kernel_size=3, padding=1,activation=relu) # path 3 self.p3_conv_1 = nn.Conv2D(n3_1, kernel_size=1,activation=relu) self.p3_conv_5 = nn.Conv2D(n3_5, kernel_size=5, padding=2,activation=relu) # path 4 self.p4_pool_3 = nn.MaxPool2D(pool_size=3, padding=1,strides=1) self.p4_conv_1 = nn.Conv2D(n4_1, kernel_size=1,activation=relu) def forward(self, x): p1 = self.p1_conv_1(x) p2 = self.p2_conv_3(self.p2_conv_1(x)) p3 = self.p3_conv_5(self.p3_conv_1(x)) p4 = self.p4_conv_1(self.p4_pool_3(x)) return nd.concat(p1, p2, p3, p4, dim=1)
其中,path1,path2,path3,path4表示的是從Input到Concat的四條輸出。
然後dim=1的原因是,我們知道卷積層的輸入和輸出都是4D的數據:
batch_size * channels * height * width
那麼,Inception的作用是對channel層進行融合,那麼就得在dim=1的維度進行concat
下面,通過實例化這個Inception,看看得到的結果。
incp = Inception(64,96,128,16,32,32)incp.initialize()x = nd.random.uniform(shape = (32,3,64,64))print (incp(x).shape)
輸出的信息是(32,256,64,64),相比較輸入的(32,3,64,64),確實起到了信息融合和升高維度的作用。
其中的256是這樣算出來的,256 = 64+128+32+32
定義GoogLeNet
其實,GoogLeNet就是將數個Inception模塊串聯起來構成的一個大的網路,為了可以更方便的觀察我們input進去的數據在內部是如何發生變化的,我們對每一個塊,使用nn.Sequential,然後再把所有這些塊都串聯起來。。
class GoogLeNet(nn.Block): def __init__(self,num_classes,verbose = False, **kwargs): super(GoogLeNet,self).__init__(**kwargs) self.verbose = verbose with self.name_scope(): # block_1 b1 = nn.Sequential() b1.add( nn.Conv2D(64,kernel_size = 7,strides = 2,padding = 3, activation = relu), nn.MaxPool2D(pool_size = 3,strides = 2) ) # block_2 b2 = nn.Sequential() b2.add( nn.Conv2D(64,kernel_size = 1), nn.Conv2D(192,kernel_size = 3,padding = 1), nn.MaxPool2D(pool_size = 3,strides = 2) ) # block_3 b3 = nn.Sequential() b3.add( Inception(64,96,128,16,32,32), Inception(128, 128, 192, 32, 96, 64), nn.MaxPool2D(pool_size = 3, strides = 2) ) # block_4 b4 = nn.Sequential() b4.add( Inception(192, 96, 208, 16, 48, 64), Inception(160, 112, 224, 24, 64, 64), Inception(128, 128, 256, 24, 64, 64), Inception(112, 144, 288, 32, 64, 64), Inception(256, 160, 320, 32, 128, 128), nn.MaxPool2D(pool_size = 3,strides = 2) ) # block_5 b5 = nn.Sequential() b5.add( Inception(256, 160, 320, 32, 128, 128), Inception(384, 192, 384, 48, 128, 128), nn.AvgPool2D(pool_size = 2) ) # block_6 b6 = nn.Sequential() b6.add( nn.Flatten(), nn.Dense(num_classes) ) self.net = nn.Sequential() self.net.add(b1,b2,b3,b4,b5,b6) def forward(self,x): out = x for i,b in enumerate(self.net): out = b(out) if self.verbose: print (Block %d output: %s %(i+1,out.shape)) return out
然後,我們來測試寫這個GoogLeNet結構,看看其中每個快對輸出的變化情況
net = GoogLeNet(10,verbose = True)net.initialize()x = nd.random.uniform(shape = (4,3,96,96))y = net(x)print (y)
我們輸入的是(4,3,96,96),其中batch_size = 4表示有4個sample,channel為3,height和width都是96的。
然後,我們print出y的信息。
Block 1 output: (4, 64, 23, 23)
Block 2 output: (4, 192, 11, 11)
Block 3 output: (4, 480, 5, 5)
Block 4 output: (4, 832, 2, 2)
Block 5 output: (4, 1024, 1, 1)
Block 6 output: (4, 10)
其中,通過一次一次的變化,最終得到的是(4,10),也就是4個sample,每個sample對應了矩陣中的一行,10個類別就對應了每一行中的10個列。
從Block1到Block5,我們可以觀察到channels的個數在不斷的上升,height和width的尺寸在不斷的減小,最終,經過一個global average pooling得到了1024個channel,1*1尺寸的feature map。
然後再接一個全連接網路,變成了4*10的輸出
結論
GoogLeNet其實就是加入更加結構化的Inception塊來使得我們可以使用更大的通道,更多的層,同時控制模型的計算量和模型大小在合理的範圍內。
ResNet:深度殘差網路
該項工作可以說,很大程度上改變了deep learning大部分的工作,已經成為幾乎大部分工作的主流架構了。很多取得不錯性能的工作都是藉助resnet來完成的。這一部分,就給小夥伴們好好講講什麼是resnet,以及resnet的最簡單的架構。該架構可以構建出一個更深更簡單的網路結構。
ResNet有效的解決了深度卷積神經網路中的網路難以訓練的問題。這是因為,我們在跑bp演算法的過程中,從第i+1層,向第i層傳遞的是誤差相對於第i+1層與第i層之間的權重的導數,對於這個傳遞的量,我們可以更好的理解成為信息量。網路中的每一個parameter都需要經過信息量來進行update,如果信息量不夠或者說信息量幾乎等於0了,那麼此時對應層的parameter就沒辦法得到update。這就導致了距離輸出層越遠的網路,就越加難以訓練,隨著網路層數的增加,這種情況越加明顯。這也就是我在模式識別專欄中寫的出現了「網路麻痹」的現象。
在ResNet出現之前,有兩種常用的方法被用來解決這個問題:
- 逐層訓練。先訓練靠近input的層,然後逐漸增加層的個數,但是效果並不是很好,而且實現起來,非常麻煩
- 使用更加wide的網路,而不是通過更deep的網路,這樣做的會使得網路的複雜度上升很快,並且更wide的網路並沒有更deep的網路表現的好
而ResNet就通過增加逐層的連接來解決梯度逐層回傳時,逐漸變小的問題。這應該就是以前出現的high-way的思路,但是只有ResNet真正把它做好了。
下圖展示了一個跨層的連接:
最底下那層的輸入不僅僅是輸出給了中間的那個藍色的層,而且還通過與中間層的結果相加進入了最上面的一層。
這樣做的一個好處就是:我們在進行誤差梯度反傳的過程中,可以不通過中間層,直接將誤差梯度傳回至輸入層,這樣就達到了避免輸入層梯度接收到誤差梯度過小的情況。
現在,我們將上述的這張圖拆分成兩個部分,從而更加清楚的看清到底ResNet是怎麼個過程。
拆分成上面的兩個網路的和。
- 其中一個網路是一個layer的,對應第一個圖中連線的部分。
- 其中第二個網路是兩個layer的,對應的是第一個圖中,直接通過輸入層,然後送入中間層的部分
其實,最下面這個layer的權重是這兩個網路所共享的。
在訓練的時候,左邊的網路因為簡單,所以非常好訓練。這個網路沒有抓取到的信息量,我們就可以叫做殘差,在通過右邊這個複雜的網路來訓練。
所以,直觀上說。即使加深網路的深度,底層的網路肯定是可以被充分訓練的,也不會出現網路訓練越來越難訓練的問題。
Residual塊
ResNet沿用了VGG中使用的3*3的卷積,但是在卷積層和池化層之間加入了BN來加速網路的訓練。每次跨層連接跨過兩個卷積。
這裡,我們定義一下殘差塊的概念。
有一個比較重要的概念,就是如果我們輸入的通道數和輸出的通道數不一致,也就是後面代碼中的same_shape = False,那麼,這個時候我們就得使用NIN中介紹的1*1的卷積來進行channel層面的變化,同時使用strides = 2 來改變height和width的尺寸。
就相當於前面講過的,要使長方體的體積(信息量)不變,高變換了,就得改變其底面積來適應這種變化。
class Residual(nn.Block): def __init__(self,channels,same_shape = True, **kwargs): super(Residual,self).__init__(**kwargs) self.same_shape = same_shape strides = 1 if same_shape else 2 self.conv1 = nn.Conv2D(channels,kernel_size = 3,padding = 1,strides = strides) self.bn1 = nn.BathNorm() self.conv2 = nn.Conv2D(channels,kernel_size = 3,padding = 1,strides = strides) self.bn2 = nn.BatchNorm() if not same_shape: self.conv3 = nn.Conv2D(channels,kernel_size = 1,strides = strides) def forward(self,x): out = nd.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) if not self.same_shape: x = self.conv3(x) return nd.relu(out+x)
- 輸入輸出通道相同:
blk = Residual(3)blk.initialize()x = nd.random.uniform(shape=(4,3,6,6))y = blk(x)print (y.shape)
我們輸入的是(4,3,6,6),最終得到的也是(4,3,6,6)。那麼就驗證了輸入和輸出通道相同。
2. 輸入和輸出不同的時候:
blk2 = Residual(8,same_shape=False)blk2.initialize()x = nd.random.uniform(shape = (4,3,6,6))y2 = blk2(x)print (y2.shape)
輸入的是(4,3,6,6),輸出都是(4,8,3,3),可以觀察到,通過將輸入的height和width降低,然後增加channels的方法,維持原有的信息量。
構建ResNet
類似GoogLeNet的主體是串聯多個Inception模塊,那麼ResNet的主體就是串聯多個Residual模塊。下面,我們自己定義一個18層的ResNet。同樣為了閱讀更加容易,我們還是使用先前介紹過的nn.Sequential堆疊的形式。這裡,我們沒有使用池化層來改變每層數據的height和width,而是通過有channel變化的Residual模塊的strides=2的卷積層來做的。。。
class ResNet(nn.Block): def __init__(self,num_classes,verbose = False,**kwargs): super(ResNet,self).__init__(**kwargs) self.verbose = verbose with self.name_scope(): # block_1 b1 = nn.Conv2D(64,kernel_size = 7, strides = 2) # block_2 b2 = nn.Sequential() b2.add( nn.MaxPool2D(pool_size = 3,strides = 2), Residual(64), Residual(64) ) # block_3 b3 = nn.Sequential() b3.add( Residual(128,same_shape = False), Residual(128) ) # block_4 b4 = nn.Sequential() b4.add( Residual(256,same_shape= False), Residual(256) ) # block_5 b5 = nn.Sequential() b5.add( Residual(512,same_shape=False), Residual(512) ) # block_6 b6 = nn.Sequential() b6.add( nn.AvgPool2D(pool_size = 3), nn.Dense(num_classes) ) self.net = nn.Sequential() self.net.add(b1,b2,b3,b4,b5,b6) def forward(self,x): out = x for i, b in enumerate(self.net): out = b(out) if self.verbose: print (Block %d output: %s%(i+1,out.shape)) return out
接下來,通過實例化,來看看這個例子。
net = ResNet(10,verbose=True)net.initialize()x = nd.random.uniform(shape = (4,3,96,96))y = net(x)print (y.shape)
輸入的是(4,3,96,96),輸出的結果是
Block 1 output: (4, 64, 45, 45)
Block 2 output: (4, 64, 22, 22)
Block 3 output: (4, 128, 11, 11)
Block 4 output: (4, 256, 6, 6)
Block 5 output: (4, 512, 3, 3)
Block 6 output: (4, 10)
(4, 10)
看到最終的第5個block的結果是(4,512,3,3)。然後通過一個global average pooling+全連接層得到了最終的(4,10)
DenseNet:稠密連接的卷積神經網路
ResNet的工作過後,後面又出現了叫做DenseNet的工作,其實方法和創新上大同小異。
可以看出來,ResNet里,其實是將跳層的輸出通過相加的方式來得到下一層的輸入。
而DenseNet是通過將跳層的輸出通過concat的方式,來得到下一層的輸入。因為是concat的操作,切記,是對channel層的拼接,所以底層的輸出會保留的進入上面所有層。這就是叫DenseNet的原因了。
稠密塊 (Dense Block)
DenseNet的卷積塊使用ResNet改進版本的BN->Relu->Conv。其中,每個卷積的輸出通道被稱為growth_rate。這是因為,我們假設輸出為in_channels,而且還有各個layer層的緣故。
那麼,輸出的通道數就是in_channels+layers*growth_rate,下面通過代碼和實例來為讀者展現這個過程。
def conv_block(channels): out = nn.Sequential() out.add( nn.BatchNorm(), nn.Activation(relu), nn.Conv2D(channels, kernel_size=3, padding=1) ) return outclass DenseBlock(nn.Block): def __init__(self, layers, growth_rate, **kwargs): super(DenseBlock, self).__init__(**kwargs) self.net = nn.Sequential() for i in range(layers): self.net.add(conv_block(growth_rate)) def forward(self, x): for layer in self.net: out = layer(x) x = nd.concat(x, out, dim=1) return x dblk = DenseBlock(2,10)dblk.initialize()x = nd.random.uniform(shape=(4,3,8,8))print (dblk(x).shape)
其中的conv_block是通過BN->Relu->Conv的結構。其中,每個卷積的輸出通道被稱為growth_rate。通過DenseBlock,我們看到其將輸入的growth_rate = 10,且layer = 2,並且在forward的過程中,dim = 1,那麼最終concat的結果就是10*2+3 = 23 ,3是因為輸入是(4,3,8,8)的,並且,concat的過程是對channel的層級進行concat。
使用print得到的輸出結果是(4,23,8,8)
過渡塊(Transition Block)
通過前面conv_block和DenseBlock的定義,我們知道了其是對dim=1,也就是channel級別的concat,並且每次concat都是底層所有輸入的concat,這樣不停的concat下去,,,
channel的個數肯定會不斷的激增。為了控制模型的複雜度,這裡引入了一個過渡塊,它的作用:
- 不僅可以使得height和width的尺寸減半
- 同時可以使用1*1的卷積來改變通道數
def transition_block(channels): out = nn.Sequential() out.add( nn.BatchNorm(), nn.Activation(relu), nn.Conv2D(channels,kernel_size = 1), nn.AvgPool2D(pool_size = 2,strides = 2) ) return out tblk = transition_block(10)tblk.initialize()print (tblk(x).shape)
通過指定輸出的channel為10,我們最終print的結果是(4,10,4,4),把(4,3,8,8)的輸入變成了(4,10,4,4)的大小。
DenseNet
DenseNet的主體就是交替串聯稠密塊和過渡塊,這兩個block,前面的代碼已經詳細描述了。
然後在使用一個全局的growth_rate使得其配置更加簡單。過渡層每次都是將channel的個數減半。下面,我們定義一個121層的DenseNet。
init_channels = 64growth_rate = 32block_layers = [6, 12, 24, 16]num_classes = 10def dense_net(): net = nn.Sequential() # first block like ResNet net.add( nn.Conv2D(init_channels,kernel_size = 7,strides = 2, padding = 3), nn.BatchNorm(), nn.Activation(relu), nn.MaxPool2D(pool_size = 3,strides = 2, padding = 1) ) # dense Blocks channels = init_channels for i, layer in enumerate(block_layers): net.add(DenseBlock(layers,growth_rate)) channels += layers*growth_rate if i != len(block_layers)-1: net.add(transition_block(channels//2)) # last Blocks net.add( nn.BatchNorm(), nn.Activation(relu), nn.AvgPool2D(pool_size = 1), nn.Flatten(), nn.Dense(num_classes) ) return net
圖片增強
AlexNet當年取得ImageNet的冠軍,其中一個方面是其使用了非常巧妙的網路設計結構,還有一個重要作用就是做Data Augmentation。
圖片增強通過一系列的隨機變化生成大量「新」樣本,從而降低過擬合的可能。在目前不管是做計算機視覺的研究還是比賽,都使用到了圖片增強技術。
常用的圖片增強方法
我們先讀取一張400*500的圖片作為樣例
from mxnet import ndarray as ndfrom mxnet import gluonfrom mxnet import autogradfrom mxnet.gluon import nnimport mxnet as mximport matplotlib.pyplot as pltfrom mxnet import imageimport utilsimg = image.imdecode(open(cat1.jpg,rb).read())plt.imshow(img.asnumpy())
接下來,我們為了以後的操作方便,定義一個輔助函數,在給定輸入圖片img的增廣方法aug後,它會運行多次並畫出結果來。
def apply(img,aug,n=3): X = [aug(img.astype(float32)) for _ in range(n*n)] Y = nd.stack(*X).clip(0,255)/255 utils.show_images(Y,n,n,figsize=(8,8))aug = image.HorizontalFlipAug(0.5)apply(img,aug)
一定要記住,在對圖像進行操作之前,一定要將其轉成float類型的,因為原始默認的圖像是int8類型的,進行某些操作,比如除法什麼的時候,會涉及到小數的運算。所有,float類型的數據更方便處理。
顯示浮點輸入的時候,image show要求輸入在[0,1]之間。
所以,為了保證輸入值是合法值,所以做一次clip
變形
水平方向翻轉圖片是一種比較常見的增廣方式,這種方式對於人來說,確實沒什麼,但是對於機器來說,就可以很好地「騙過」機器,從而增加了樣本的個數
比如,下面的代碼,以0.5的概率做翻轉
aug = image.HorizontalFlipAug(0.5)apply(img,aug)
隨機剪裁:
隨機剪裁一個塊200*200的區域
aug = image.RandomCropAug([200,200])apply(img,aug)
隨機剪裁,要求保留至少0.1的區域,並且隨機長寬比在0.5和2之間
最後,再將結果resize到200*200
aug = image.RandomSizedCropAug((200,200), .1, (.5,2))apply(img, aug)
顏色變化
形狀變化外的另一個大類是顏色變化。
隨機將亮度增加或者減小在0-50%間的一個量
aug = image.BrightnessJitterAug(0.5)apply(img,aug)
隨機色調變化
aug = image.HueJitterAug(0.5)apply(img,aug)
這一講就到這裡結束吧,下一篇文章,我們要好好調一調CIFAR-10這個數據集,然後在kaggle上試試結果,當做自己成為初級煉丹師的一個必經之路吧。。
推薦閱讀:
TAG:深度學習DeepLearning | MXNet |