MXNET深度學習框架-19-從0開始的Bathnorm(批量歸一化)

         通常來說,數據標準化預處理對於淺層模型就足夠有效了。隨着模型訓練的進行,當每層中參數更新時,靠近輸出層的輸出較難出現劇烈變化。但對深層神經網絡來說,即使輸入數據已做標準化,訓練中模型參數的更新依然很容易造成靠近輸出層輸出的劇烈變化。這種計算數值的不穩定性通常令我們難以訓練出有效的深度模型。

         批量歸一化(batch normalization)是一個能讓較深的神經網絡的訓練變得更加容易的方法。在模型訓練時,批量歸一化利用小批量上的均值和標準差,不斷調整神經網絡中間輸出,從而使整個神經網絡在各層的中間輸出的數值更穩定。簡單來說,batch normalization就是對神經網絡某一層的激活函數的輸入進行歸一化,使批量呈標準正態分佈(均值爲0,標準差爲1)

相關公式如下:
         給定一個批量B={x1,...,mx_1,...,m},我們需要學習拉伸參數γ\gammaβ\beta
         所以定義:

                                    (1) 1mi=1mxiμB\frac{1}{m}\sum_{i=1}^m x_i\to\mu_B

                                    (2) 1mi=1m(xiμB)2σB2\frac{1}{m}\sum_{i=1}^m (x_i-\mu_B)^2\to\sigma_B^2

                                    (3) xiμBσB2+ϵx^i\frac{x_i- \mu_B}{\sqrt{\sigma_B^2 +\epsilon}}\to\hat x_i

                                    (4) BNγ,β(xi)γx^i+βyiBN_\gamma,_\beta (x_i)\equiv\gamma\hat x_i+\beta\to y_i

         其中,(1)式求出均值;(2)式求出方差;(3)式:(輸入-均值)/方差,加上ϵ\epsilon是爲了保證分母不小於0,最終稿得到一個均化的xx;(4)式:γ\gammaβ\beta就是在訓練過程中進行更新變化,有點類似wwbb

根據公式我們來實現一個簡單的批量歸一化層:

# BN要考慮全連接層和卷積層,當爲卷積層時,需要對每個通道進行歸一化,當爲全連接層時,
# 每個批量都要使用
def batch_norm(X,gamma,beta,eps=1e-5):
    # len(X.shape) in (2,4) # 就是說X如果是全連接,那麼就是2維,卷積是一個4維的東西,所以是4
    if len(X.shape)==2:
        # 每個輸入維度在樣本上的均值和方差 (batch_size*feature)
        mean=X.mean(axis=0) # 均值
        vari=((X-mean)**2).mean(axis=0) # 方差
    else:
        # 2D卷積(NCHW--batch_size*channel*height*width)
        mean=X.mean(axis=(0,2,3),keepdims=True)  
        vari=((X-mean)**2).mean(axis=(0,2,3),keepdims=True) 
    # 均一化
    X_hat=(X-mean)/nd.sqrt(vari+eps) 
    #拉昇和偏移(維度一致,便於計算)
    return gamma.reshape(mean.shape)*X_hat+beta.reshape(mean.shape) 

1)運行一個全連接的實例

A=nd.arange(6).reshape((3,2))
print(A.shape)
result=batch_norm(A,gamma=nd.array([1,1]),beta=nd.array([0,0])) # A是一個3X2的矩陣,gamma是一個1X2的矩陣
print(result)

結果:
在這裏插入圖片描述
我們的預期結果是對每一列進行歸一化,下面我們來分析一下,看這一列:
在這裏插入圖片描述
我們可以目測發現:均值爲0,方差爲1,符合結果。

2)運行一個2D卷積的實例

B=nd.arange(18).reshape((1,2,3,3))
print(B)
result_conv=batch_norm(B,gamma=nd.array([1,1]),beta=nd.array([0,0]))
print(result_conv)

運行結果:
在這裏插入圖片描述
         同樣符合預期結果,均值爲0,方差爲1。
         以上爲訓練過程所使用的batch norm,那麼,測試的時候要不要使用BN?這個問題其實不太好回答:
         1、不用的話訓練出的模型參數在測試時也許就不準了;
         2、用的話,測試時有可能只有一個數據。
         事實上,測試時也是需要使用BN的,只不過需要改動一下。在測試時,需要把原先訓練的均值和方差替換成整個訓練數據的均值和方差,但是當訓練數據很大時,計算量就非常大,所以,使用移動平均的方法來近似計算。

這裏有個思路:在訓練時存儲全局的均值和方差,測試時使用就好了。所以更新一下BN代碼:

def BN(X,gamma,beta,is_training,moving_mean,moving_vari,moving_momentum,eps=1e-5):
    # len(X.shape) in (2,4) # 就是說X如果是全連接,那麼就是2維,卷積是一個4維的東西,所以是4
    if len(X.shape) == 2:
        # 每個輸入維度在樣本上的均值和方差 (batch_size*feature)
        mean = X.mean(axis=0)  # 均值
        vari = ((X - mean) ** 2).mean(axis=0)  # 方差
    else:
        # 2D卷積(NCHW--batch_size*channel*height*width)
        mean = X.mean(axis=(0, 2, 3), keepdims=True)  
        vari = ((X - mean) ** 2).mean(axis=(0, 2, 3), keepdims=True)  
        moving_mean=moving_mean.reshape(mean.shape) # 維度需要一致
        moving_vari=moving_vari.reshape(vari.shape)
    # 均一化
    if is_training:
        X_hat = (X - mean) / nd.sqrt(vari + eps)  
        # 更新全局的均值和方差
        moving_mean[:]=moving_momentum*moving_mean+(1-moving_momentum)*mean # moving_momentum--平滑因子
        moving_vari[:]=moving_momentum*moving_vari+(1-moving_momentum)*vari
    else:
        # 測試時使用全局均值和方差
        X_hat = (X - moving_mean) / nd.sqrt(moving_vari + eps)  
        #拉昇和偏移(維度一致,便於計算)
    return gamma.reshape(mean.shape)*X_hat+beta.reshape(mean.shape) # 

BN應用到CNN中

         這裏需要提前說明一點,BN只能讓模型收斂加快,對於提升準確率的方法需要查找其它方法。

import mxnet.ndarray as nd
import mxnet.autograd as ag
import mxnet.gluon as gn
import mxnet as mx
'''
# BN要考慮全連接層和卷積層,當爲卷積層時,需要對每個通道進行歸一化,當爲全連接層時,
# 每個批量都要使用
def batch_norm(X,gamma,beta,eps=1e-5):
    # len(X.shape) in (2,4) # 就是說X如果是全連接,那麼就是2維,卷積是一個4維的東西,所以是4
    if len(X.shape)==2:
        # 每個輸入維度在樣本上的均值和方差 (batch_size*feature)
        mean=X.mean(axis=0) # 均值
        vari=((X-mean)**2).mean(axis=0) # 方差
    else:
        # 2D卷積(NCHW--batch_size*channel*height*width)
        mean=X.mean(axis=(0,2,3),keepdims=True)  # CSDN對應的公式(1)
        vari=((X-mean)**2).mean(axis=(0,2,3),keepdims=True) # CSDN對應的公式(2)
    # 均一化
    X_hat=(X-mean)/nd.sqrt(vari+eps) # CSDN對應的公式(3)
    #拉昇和偏移(維度一致,便於計算)
    return gamma.reshape(mean.shape)*X_hat+beta.reshape(mean.shape) # CSDN對應的公式(4)
# 運行一個實例看看(全連接的實例)
A=nd.arange(6).reshape((3,2))
print(A)
result=batch_norm(A,gamma=nd.array([1,1]),beta=nd.array([0,0])) # A是一個3X2的矩陣,gamma是一個1X2的矩陣
print(result)
# 運行一個實例看看(卷積的實例)
B=nd.arange(18).reshape((1,2,3,3))
print(B)
result_conv=batch_norm(B,gamma=nd.array([1,1]),beta=nd.array([0,0]))
print(result_conv)
'''
def BN(X,gamma,beta,is_training,moving_mean,moving_vari,moving_momentum=0.9,eps=1e-5):
    # len(X.shape) in (2,4) # 就是說X如果是全連接,那麼就是2維,卷積是一個4維的東西,所以是4
    if len(X.shape) == 2:
        # 每個輸入維度在樣本上的均值和方差 (batch_size*feature)
        mean = X.mean(axis=0)  # 均值
        vari = ((X - mean) ** 2).mean(axis=0)  # 方差
    else:
        # 2D卷積(NCHW--batch_size*channel*height*width)
        mean = X.mean(axis=(0, 2, 3), keepdims=True)  # CSDN對應的公式(1)
        vari = ((X - mean) ** 2).mean(axis=(0, 2, 3), keepdims=True)  # CSDN對應的公式(2)
        moving_mean=moving_mean.reshape(mean.shape) # 維度需要一致
        moving_vari=moving_vari.reshape(vari.shape)
    # 均一化
    if is_training:
        X_hat = (X - mean) / nd.sqrt(vari + eps)  # CSDN對應的公式(3)
        # 更新全局的均值和方差
        moving_mean[:]=moving_momentum*moving_mean+(1-moving_momentum)*mean # moving_momentum--平滑因子
        moving_vari[:]=moving_momentum*moving_vari+(1-moving_momentum)*vari
    else:
        # 測試時使用全局均值和方差
        X_hat = (X - moving_mean) / nd.sqrt(moving_vari + eps)  # CSDN對應的公式(3)
        #拉昇和偏移(維度一致,便於計算)
    return gamma.reshape(mean.shape)*X_hat+beta.reshape(mean.shape) # CSDN對應的公式(4)

ctx=mx.gpu()
'''---參數定義---'''
# 1、創建第一個卷積層的w、b ,核大小爲5,單通道圖片(輸入),輸出20個特徵圖
c1,c2=20,50
w1 = nd.random_normal(shape=(c1, 1, 5, 5), scale=0.01,ctx=ctx)
b1 = nd.random_normal(shape=w1.shape[0], scale=0.01, ctx=ctx)
# BN
gamma1=nd.random_normal(shape=(c1),scale=0.01,ctx=ctx) #如果是卷積的話,gamma和beta的數目跟上一層卷積輸出特徵圖數是一樣的
beta1=nd.random_normal(shape=(c1),scale=0.01,ctx=ctx)
moving_mean1=nd.zeros(shape=c1,ctx=ctx)
moving_vari1=nd.zeros(shape=c1,ctx=ctx)
# 2、創建第二個卷積層的w、b ,核大小爲5,輸入爲20,輸出50個特徵圖
w2 = nd.random_normal(shape=(c2, c1, 3, 3), scale=0.01, ctx=ctx)
b2 = nd.random_normal(shape=w2.shape[0], scale=0.01, ctx=ctx)
# BN
gamma2=nd.random_normal(shape=(c2),scale=0.01,ctx=ctx)
beta2=nd.random_normal(shape=(c2),scale=0.01,ctx=ctx)
moving_mean2=nd.zeros(shape=c2,ctx=ctx)
moving_vari2=nd.zeros(shape=c2,ctx=ctx)
#  3、全連接層參數------(1250,128)
f3=128
w3 = nd.random_normal(shape=(1250, f3), scale=0.01, ctx=ctx)
b3 = nd.random_normal(shape=f3, scale=0.01, ctx=ctx)
#  4、全連接層參數------(128,10)
w4 = nd.random_normal(shape=(f3, 10), scale=0.01, ctx=ctx)
b4 = nd.random_normal(shape=10, scale=0.01, ctx=ctx)
params=[w1, b1, w2, b2, w3, b3, w4, b4,gamma1,beta1,gamma2,beta2] # moving_mean和moving_vari是不需要更新的
for param in params:
    param.attach_grad()
'''---定義模型(注意BN是在卷積之後,激活函數之前)---'''
def cnn_net(X,is_training=False):# 這裏只對卷積做BN
    # 第一層卷積+池化

    h1_conv = nd.Convolution(data=X.reshape((-1, 1, 28, 28)), weight=w1, bias=b1, kernel=w1.shape[2:],
                             num_filter=w1.shape[0])
    h1_bn=BN(h1_conv,gamma1,beta1,is_training,moving_mean1,moving_vari1)
    h1_activity = nd.relu(data=h1_bn)  # relu激活
    h1_pool = nd.Pooling(data=h1_activity, kernel=(2, 2), pool_type="max", stride=(2, 2))  # 最大池化
    # 第二層卷積+池化
    h2_conv = nd.Convolution(data=h1_pool, weight=w2, bias=b2, kernel=w2.shape[2:], num_filter=w2.shape[0])
    h2_bn = BN(h2_conv, gamma2, beta2, is_training, moving_mean2, moving_vari2)
    h2_activity = nd.relu(data=h2_bn)  # relu激活
    h2_pool = nd.Pooling(data=h2_activity, kernel=(2, 2), pool_type="max", stride=(2, 2))  # 最大池化
    # 第一層全連接
    h2 = nd.Flatten(h2_pool)  # 拉成一個batch*一維向量(2D矩陣)
    h3_linear = nd.dot(h2, w3) + b3
    h3 = nd.relu(h3_linear)
    # 第二層全連接
    h4 = nd.dot(h3, w4) + b4
    return h4

'''---讀取數據和預處理---'''
def load_data_fashion_mnist(batch_size, resize=None):
    transformer = []
    if resize:
        transformer += [gn.data.vision.transforms.Resize(resize)]
    transformer += [gn.data.vision.transforms.ToTensor()]
    transformer = gn.data.vision.transforms.Compose(transformer)
    mnist_train = gn.data.vision.FashionMNIST( train=True)
    mnist_test = gn.data.vision.FashionMNIST( train=False)
    train_iter = gn.data.DataLoader(
        mnist_train.transform_first(transformer), batch_size, shuffle=True)
    test_iter = gn.data.DataLoader(
        mnist_test.transform_first(transformer), batch_size, shuffle=False)
    return train_iter, test_iter

batch_size=128
train_iter,test_iter=load_data_fashion_mnist(batch_size)
cross_loss = gn.loss.SoftmaxCrossEntropyLoss()


# 定義準確率
def accuracy(output, label):
    return nd.mean(output.argmax(axis=1) == label).asscalar()


def evaluate_accuracy(data_iter, net):  # 定義測試集準確率
    acc = 0
    for data, label in data_iter:
        data,label = data.as_in_context(ctx),label.as_in_context(ctx)
        label = label.astype('float32')
        output = net(data)
        acc += accuracy(output, label)
    return acc / len(data_iter)


# 梯度下降優化器
def SGD(params, lr):
    for pa in params:
        pa[:] = pa - lr * pa.grad  # 參數沿着梯度的反方向走特定距離

# 訓練
lr = 0.2
epochs = 5
for epoch in range(epochs):
    train_loss = 0
    train_acc = 0
    for image, y in train_iter:
        image,y = image.as_in_context(ctx),y.as_in_context(ctx)
        y = y.astype('float32')
        with ag.record():
            output = cnn_net(image,is_training=True)
            loss = cross_loss(output, y)
        loss.backward()
        # 將梯度做平均,這樣學習率不會對batch_size那麼敏感
        SGD(params, lr / batch_size)
        train_loss += nd.mean(loss).asscalar()
        train_acc += accuracy(output, y)
        test_acc = evaluate_accuracy(test_iter, cnn_net)
    print("Epoch %d, Loss:%f, Train acc:%f, Test acc:%f"
          % (epoch, train_loss / len(train_iter), train_acc / len(train_iter), test_acc))

訓練結果:
在這裏插入圖片描述
這裏就不繼續跑下去了,機器太渣…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章