4 months ago

1.引入

对输入数据做了归一化处理,就是将每个特征在所有样本上的值转归一化成均值0方差1。
这样我们保证训练数据里数值都同样量级上,从而使得训练的时候数值更加稳定。
对于浅层模型来说,通常数据归一化预处理足够有效。输出数值在只经过几个神经层后通常不会出现剧烈变化。
但对于深层神经网络来说,情况一般比较复杂。因为每一层里都对输入乘以权重后得到输出。
当很多层这样的相乘累计在一起时,一个输出数据较大的改变都可以导致输出产生巨大变化,从而带来不稳定性。

批量归一化层的提出是针对这个情况。它将一个批量里的输入数据进行归一化然后输出。
如果我们将批量归一化层放置在网络的各个层之间,那么就可以不断的对中间输出进行调整,从而保证整个网络的中间输出的数值稳定性。

Batch Normalization批量归一化(简称BN)是由著名的GoogleNet首次提出
它有效地解决了在网络层数很深地情况下,收敛速度很慢地问题
使用BN操作可以加大网络梯度下降的学习率,加速网络收敛
在实际应用中,批量归一化的收敛非常快,并且具有很强的泛化能力,某种情况下完全可以替代正则化和弃权。

BN的地位:与激活函数层、卷积层、全连接层、池化层一样

BN的本质原理:在网络的每一层输入的时候(通常在激活函数前),也就是先做一个归一化处理(归一化至:均值0、方差为1)

2.原理介绍

假设有数列B,该数列的BN操作过程如下:


1)计算数列的均值
2)计算数列的方差
3)归一化处理
4)引入两个可训练参数,对数据进行缩放和平移,避免归一化处理后网络学习到的特征分布丢失
在卷积神经网络中的卷积层使用BN处理,相当于卷积计算:经过简单公式变换,可以理解为先进行depthwise_conv2d卷积,接着加偏置,再进行depthwise_conv2d卷积,再加偏置

3.使用tensorflow实现

1)计算均值和方差

tf.nn.moments(x, axes, name=None, keep_dims=False):
各参数说明如下:

x:输入张量。
axes:指定沿哪个轴计算平均值和方差。正常每个维度都要计算,所以对于一维张量一维张量就是[0],二维张量就是[0,1],三  维张量就是[0,1,2],四维张量就是[0,1,2,3]
name:名称。
keep_dims:是否保留维度,即形状是否和输入一样。

2)批量归一化

tf.nn.batch_normalization(x,mean,variance,offset,scale,variance_epsilon,name=None):
各个参数的说明如下:

x:代表任意维度的输入张量。
mean:代表样本的均值。一维张量,长度与x的深度保持一致
variance:代表样本的方差。一维张量,长度与x的深度保持一致
offset:代表偏移,即相加一个转化值,也是公式中的beta。一维张量,长度与x的深度保持一致
scale:代表缩放,即乘以一个转化值,也是公式中的gamma。
variance_epsilon:是为了避免分母为0的情况下,给分母加上的一个极小值,可以取1e-8。
name:名称。

一维张量的批量归一化

import tensorflow as tf
x=tf.constant([1,10,23,15],tf.float32)
#"计算均值和方差"
# 需要指定计算的维度:一维张量就是[0],二维张量就是[0,1],三维张量就是[0,1,2],四维张量就是[0,1,2,3]
mean,variance=tf.nn.moments(x,[0])
#"BatchNormalize"
r=tf.nn.batch_normalization(x,mean,variance,0,1,1e-8)
session=tf.Session()
print(session.run(r))

运行结果[-1.4096959 -0.28193915 1.3470427 0.34459233]
三维张量的批量归一化

x=tf.placeholder(tf.float32,[None,2,2,2])
dim=list(range(len(x.shape)-1))
mean,var=tf.nn.moments(x,dim)
bn=tf.nn.batch_normalization(x,mean,var,(3,8),(2,5),1e-8)
input=np.array([
    [[[1,12],[6,18]],[[9,13],[4,11]]],
    [[[2,19],[7,17]],[[3,15],[8,11]]]
])
sess=tf.Session()
sess.run(bn,{x:input})
sess.close()

4.批量归一化的简单用法

上面的函数虽然参数不多,但是需要几个函数联合起来使用
于是TensorFlow中的layers模块里又实现了一次BN函数,相当于把几个函数合并到了一起,使用起来更加简单
下面来介绍一下,使用时需要引入:from tensorflow.contrib.layers.python.layers import batch_norm
或者直接调用tf.contrib.layers.batch_norm()该函数的定义如下:

def batch_norm(inputs,
               decay=0.999,
               center=True,
               scale=False,
               epsilon=0.001,
               activation_fn=None,
               param_initializers=None,
               param_regularizers=None,
               updates_collections=ops.GraphKeys.UPDATE_OPS,
               is_training=True,
               reuse=None,
               variables_collections=None,
               outputs_collections=None,
               trainable=True,
               batch_weights=None,
               fused=False,
               data_format=DATA_FORMAT_NHWC,
               zero_debias_moving_mean=False,
               scope=None,
               renorm=False,
               renorm_clipping=None,
               renorm_decay=0.99):

一些参数看名字也大概知道什么了,具体详见下方第二份参考资料

5.带BN操作的卷积神经网络

这次来实现一个带有BN操作的两层神经网络

import tensorflow as tf
import numpy as tf
# 输入值占位
x=tf.placeholder(tf.float32,[None,1,2,3])
# 1.第一层神经网络
# 1)卷积后加偏置
k1=tf.constant([[[[1,0],[0,1],[0,0]]]],tf.float32)
conv1=tf.nn.conv2d(x,k1,[1,1,1,1],"SAME")
b1=tf.Variable(tf.random_normal([conv1.shape[-1].value,]))
l1=conv1+b1
# 2)BN操作
axes1=list(range(len(l1.shape)-1))
mean1,var1=tf.nn.moments(l1,axes)
bias1=tf.Variable(tf.random_normal([conv1.shape[-1].value,]))
scaler1=tf.Variable(tf.random_normal([conv1.shape[-1].value,]))
bn1=tf.nn.batch_normalization(l1,mean1,var1,bias1,scaler1,1e-8)
# 3)relu激活
act1=tf.nn.relu(bn1)
# 2.第二层神经网络
# 1)卷积后加偏置
k2=tf.constant([[[[0,2],[2,0]]]],tf.float32)
conv2=tf.nn.conv2d(act1,k2,[1,1,1,1],"SAME")
b2=tf.Variable(tf.random_normal([conv2.shape[-1].value,]))
l2=conv2+b2
# 2)BN操作
axes=list(range(len(l2.shape)-1))
mean2,var2=tf.nn.moments(l2,axes)
bias2=tf.Variable(tf.random_normal([conv2.shape[-1].value,]))
scaler2=tf.Variable(tf.random_normal([conv2.shape[-1].value,]))
bn2=tf.nn.batch_normalization(l2,mean2,var2,bias2,scaler2,1e-8)
# 3)relu激活
act2=tf.nn.relu(bn2)

# 传入输入值
input=np.array([
    [[[1,2,3],[4,5,6]]],
    [[[11,12,13],[14,15,16]]],
    [[[21,23,21],[23,24,22]]]
],np.float32)

# 创建会话
sess=tf.Session()
sess.run(tf.global_variables_initializer())
print(sess.run(act2,{x:input}))
sess.close()

6.指数移动平均

假设已经训练好一个带有BN操作的卷积神经网络,但是使用它在进行预测时,往往每次只输入一个样本
那么经过该网络是,计算单个样本的平均值和方差的意义就不大了,需要把训练集中的均值和方差拿来使用
而在训练过程中,由于mean和variance是和batch内的数据有关的,从而使得每个batch得到的均值和方差也不同
由此引入指数移动平均:把训练的过程中每个step得到的mean和variance,叠加计算对应的moving_average(滑动平均),并最终保存下来以便在predict的过程中使用

1)指数移动平均(指数加权平均)公式如下:



公式太抽象,先举一个活生生的例子:

英国伦敦365天温度数据,要求计算移动平均值,则公式中的参数实际意义如下:
Vt:1~t天的温度平均值;
β:加权系数
θt:第t天的温度
计算过程如下:(取β=0.9,V0=0,共有n天)

V0=0;
V1=0.9*V0+ 0.1*θ1;
V2=0.9*V1+ 0.1*θ2;
…
Vn=0.9*Vn-1+ 0.1*θn

2)API

(1)调用类对象
ema = tf.train.ExponentialMovingAverage(0.9)  0.9:就是其衰减因子
(2)传入参数
total=ema.apply([next1,next2,nex3...])
这里传入的参数是一个变量列表(Variable List或者Tensor List),可以同时计算多个对象的加权平均数
total1=a*total0+(1-a)*current1, total2=a*total1+(1-a)*current2
(3)开始计算:tf.Session().run(total)
(4)取值:total1=ema.average(next1)

然后来一段代码看的更清楚

import tensorflow as tf
#"初始化变量,即 t=1 时的值",注意这里的trainable
x=tf.Variable(initial_value=5,dtype=tf.float32,trainable=False,name='v')
#"创建计算移动平均的对象"
exp_moving_avg=tf.train.ExponentialMovingAverage(0.7)
#先申请函数作用于x,注意这里的参数必须是一个 Variable List 或者 Tensor List
update_moving_avg=exp_moving_avg.apply([x])
#"创建会话"
session=tf.Session()
session.run(tf.global_variables_initializer())
for i in range(4):
    #"打印指数移动平均值"
    session.run(update_moving_avg)
    print('第{}次的移动平均值:'.format(i+1),end="")
    #计算指数移动平均值,注意这里的参数必须是 A Variable object,只能从上面的apply参数中的列表里取一个
    print(session.run(exp_moving_avg.average(x)))
    session.run(x.assign_add(5))
sess.close()

运行结果:
第1次的移动平均值:5.0
第2次的移动平均值:6.5
第3次的移动平均值:9.05
第4次的移动平均值:12.335

上面这段代码看起来真是相当弱智:现实情况的原始数据肯定不是每次都+5这么简单,应该如何应对呢
关键点在于average()这个方法,特别弱智,参数只能是一个Variable对象,只能每次迭代是重新指定变量值了

import tensorflow as tf
#导入原始数据:这里为了便鉴别程序,构造了和刚刚参数相同的原始参数
x=list(range(5,21,5))
input=tf.Variable(initial_value=5.0,dtype=tf.float32,trainable=False,name="input")
exp_moving_avg=tf.train.ExponentialMovingAverage(0.7)
update=exp_moving_avg.apply([input])
sess.run(tf.global_variables_initializer())
for i in range(4):
    sess.run(tf.assign(input,x[i]))
    print("第%d次迭代:" %(i+1),"x值为",end="")
    print(sess.run(input),end=",")
    sess.run(update)
    print("mvg值为",end="")
    result=sess.run(exp_moving_avg.average(input))
    print(result)
sess.close()

经过多次调试,终于成功,这里坑比较多

7.带BN操作和指数移动平均的卷积神经网络

现在尝试将第5步 带BN操作带卷积神经网络 加上指数移动平均

#"输入值的占位符"
x=tf.placeholder(tf.float32,[None,1,2,3])
#"设置是否是训练阶段的占位符" tf.cond()需要用到这个参数
trainable=tf.placeholder(tf.bool)
#"移动平均"
ema=tf.train.ExponentialMovingAverage(0.7)
ema_var_list=[]
#"----------第一层-----------"
#"2个1行1列3深度的卷积核"
k1=tf.Variable(tf.constant([
                [[[1,0],[0,1],[0,0]]],
                ],tf.float32)
        )
#"偏置"
b1=tf.Variable(tf.zeros(2))
#"卷积结果加偏置"
c1=tf.nn.conv2d(x,k1,[1,1,1,1],'SAME')+b1
beta1=tf.Variable(tf.zeros(c1.get_shape()[-1].value))
gamma1=tf.Variable(tf.ones(c1.get_shape()[-1].value))
#"计算每一深度上的均值和方差"
m1,v1=tf.nn.moments(c1,[0,1,2])
ema_var_list+=[m1,v1]
#"为了保存均值和方差的指数移动平均"
m1_ema=tf.Variable(tf.zeros(c1.get_shape()[-1]),trainable=False)
v1_ema=tf.Variable(tf.zeros(c1.get_shape()[-1]),trainable=False)
#"BN操作" tf.cond() 类似于 if...else...
c1_BN=tf.cond(trainable,
        lambda:tf.nn.batch_normalization(c1,m1,v1,beta1,gamma1,1e-8),
        lambda: tf.nn.batch_normalization(c1,m1_ema,
                                          v1_ema,beta1,gamma1,1e-8)
        )
#"relu激活"
r1=tf.nn.relu(c1_BN)
#"----------第二层-----------"
#"2个1行1列2深度的卷积核"
k2=tf.Variable(tf.constant(
        [
        [[[2,0],[0,2]]]
        ],tf.float32
        ))
#"偏置"
b2=tf.Variable(tf.zeros(2))
#"卷积结果加偏置"
c2=tf.nn.conv2d(r1,k2,[1,1,1,1],'SAME')+b2
beta2=tf.Variable(tf.zeros(c2.get_shape()[-1]))
gamma2=tf.Variable(tf.ones(c2.get_shape()[-1]))
#"计算每一深度上的均值和方差"
m2,v2=tf.nn.moments(c2,[0,1,2])
ema_var_list+=[m2,v2]
#"为了保存均值和方差的指数移动平均"
m2_ema=tf.Variable(tf.zeros(c1.get_shape()[-1]),trainable=False)
v2_ema=tf.Variable(tf.zeros(c1.get_shape()[-1]),trainable=False)
#"BN操作"
c2_BN=tf.cond(trainable,
        lambda:tf.nn.batch_normalization(c2,m2,v2,beta2,gamma2,1e-8),
        lambda:tf.nn.batch_normalization(c2,m2_ema,
                                         v2_ema,beta2,gamma2,1e-8)
        )
#"relu激活"
r2=tf.nn.relu(c2_BN)
update_moving_avg=ema.apply(ema_var_list)
# 初始化输入值
list1=np.array([[[[1,2,3],[4,5,6]]]],np.float32)
list2=np.array([[[[11,12,13],[14,15,16]]]],np.float32)
list3=np.array([[[[21,23,21],[23,24,22]]]],np.float32)
input=(list1,list2,list3)
#"创建会话"
session=tf.Session()
session.run(tf.global_variables_initializer()) #初始化全局变量
session.run(tf.local_variables_initializer()) #初始化临时变量
coord=tf.train.Coordinator() #线程协调员
threads=tf.train.start_queue_runners(sess=session,coord=coord) #收集图中所有线程,开启线程
for i in range(len(input)):
    #arrs=session.run(i)
    print('---第%(num)d 批array---'%{'num':i+1})
    print(i)
    _,c1_arr=session.run([update_moving_avg,c1],
                        feed_dict={x:input[i],trainable:True})
    print('---第%(num)d 次迭代后第1个卷积层(卷积结果加偏置)的值---'%{'num':i+1})
    print(c1_arr)
    #"将计算的指数移动平均的值赋值给 Variable 对象"
    session.run(m1_ema.assign(ema.average(m1)))
    session.run(v1_ema.assign(ema.average(v1)))
    session.run(m2_ema.assign(ema.average(m2)))
    session.run(v2_ema.assign(ema.average(v2)))
    print('---第%(num)d 次迭代后第1个卷积层的均值的移动平均值---'%{'num':i+1})
    print(session.run(m1_ema))
coord.request_stop() #请求线程停止
coord.join(threads) #线程回收
session.close()

参考资料:
批量归一化的参数优化
第十八节,TensorFlow中使用批量归一化(BN)
译文 | 批量归一化:通过减少内部协变量转移加速深度网络训练
Optimization Algorithms优化算法
指数加权移动平均
tf.train.ExponentialMovingAverage
关于tf.train.ExponentialMovingAverage使用的详细解析
tf.cond()的用法

← 【深度学习】1x1的卷积核作用 【深度学习】GoogleNet →