我有一个关于批量归一化(以下简称BN)的理解问题。
我有一个卷积网络运行得很好,我在编写测试来检查形状和输出范围时发现,当我将批量大小设置为1时,我的模型输出为零(包括logits和激活值)。
我用BN制作了一个最简单的卷积网络原型:
输入 => 卷积 + ReLU => BN => 卷积 + ReLU => BN => 卷积层 + Tanh
模型使用泽维尔初始化进行初始化。我猜测BN在训练过程中进行了一些需要批量大小大于1的计算。
我在PyTorch中发现了一个似乎在讨论这个问题的issue:https://github.com/pytorch/pytorch/issues/1381
有谁能解释一下这是怎么回事吗?我还是有点不太明白。
运行示例:
重要提示: 运行此脚本需要Tensorlayer库:pip install tensorlayer
import tensorflow as tfimport tensorlayer as tlimport numpy as npdef conv_net(inputs, is_training): xavier_initilizer = tf.contrib.layers.xavier_initializer(uniform=True) normal_initializer = tf.random_normal_initializer(mean=1., stddev=0.02) # 输入层 network = tl.layers.InputLayer(inputs, name='input') fx = [64, 128, 256, 256, 256] for i, n_out_channel in enumerate(fx): with tf.variable_scope('h' + str(i + 1)): network = tl.layers.Conv2d( network, n_filter = n_out_channel, filter_size = (5, 5), strides = (2, 2), padding = 'VALID', act = tf.identity, W_init = xavier_initilizer, name = 'conv2d' ) network = tl.layers.BatchNormLayer( network, act = tf.identity, is_train = is_training, gamma_init = normal_initializer, name = 'batch_norm' ) network = tl.layers.PReluLayer( layer = network, a_init = tf.constant_initializer(0.2), name ='activation' ) ############# 输出层 ############### with tf.variable_scope('h' + str(len(fx) + 1)): ''' network = tl.layers.FlattenLayer(network, name='flatten') network = tl.layers.DenseLayer( network, n_units = 100, act = tf.identity, W_init = xavier_initilizer, name = 'dense' ) ''' output_filter_size = tuple([int(i) for i in network.outputs.get_shape()[1:3]]) network = tl.layers.Conv2d( network, n_filter = 100, filter_size = output_filter_size, strides = (1, 1), padding = 'VALID', act = tf.identity, W_init = xavier_initilizer, name = 'conv2d' ) network = tl.layers.BatchNormLayer( network, act = tf.identity, is_train = is_training, gamma_init = normal_initializer, name = 'batch_norm' ) net_logits = network.outputs network.outputs = tf.nn.tanh( x = network.outputs, name = 'activation' ) net_output = network.outputs return network, net_output, net_logitsif __name__ == '__main__': tf.logging.set_verbosity(tf.logging.DEBUG) ################################################# # 模型定义 # ################################################# PLH_SHAPE = [None, 256, 256, 3] input_plh = tf.placeholder(tf.float32, PLH_SHAPE, name='input_placeholder') convnet, net_out, net_logits = conv_net(input_plh, is_training=True) with tf.Session() as sess: tl.layers.initialize_global_variables(sess) convnet.print_params(details=True) ################################################# # 启动运行 # ################################################# for BATCH_SIZE in [1, 2]: INPUT_SHAPE = [BATCH_SIZE, 256, 256, 3] batch_data = np.random.random(size=INPUT_SHAPE) output, logits = sess.run( [net_out, net_logits], feed_dict={ input_plh: batch_data } ) if tf.logging.get_verbosity() == tf.logging.DEBUG: print("\n\n###########################") print("\n批量大小 = %d\n" % BATCH_SIZE) tf.logging.debug("输出 => 形状: %s - 均值: %e - 标准差: %f - 最小值: %f - 最大值: %f" % ( output.shape, output.mean(), output.std(), output.min(), output.max() )) tf.logging.debug("logits => 形状: %s - 均值: %e - 标准差: %f - 最小值: %f - 最大值: %f" % ( logits.shape, logits.mean(), logits.std(), logits.min(), logits.max() )) if tf.logging.get_verbosity() == tf.logging.DEBUG: print("###########################")
输出如下:
###########################批量大小 = 1DEBUG:tensorflow:输出 => 形状: (1, 1, 1, 100) - 均值: 0.000000e+00 - 标准差: 0.000000 - 最小值: 0.000000 - 最大值: 0.000000DEBUG:tensorflow:logits => 形状: (1, 1, 1, 100) - 均值: 0.000000e+00 - 标准差: 0.000000 - 最小值: 0.000000 - 最大值: 0.000000######################################################批量大小 = 2DEBUG:tensorflow:输出 => 形状: (2, 1, 1, 100) - 均值: -1.430511e-08 - 标准差: 0.760749 - 最小值: -0.779634 - 最大值: 0.779634DEBUG:tensorflow:logits => 形状: (2, 1, 1, 100) - 均值: -4.768372e-08 - 标准差: 0.998715 - 最小值: -1.044437 - 最大值: 1.044437###########################
回答:
你可能应该阅读一些关于批量归一化的解释,比如这个。你也可以看看TensorFlow的相关文档。
基本上,你可以用两种方式进行批量归一化,但这两种方式在处理批量大小为1时都有问题:
-
使用移动均值和方差,像素级别处理,因此它们是与批次中每个样本形状相同的张量。这是在@layog的回答中使用的方法,我认为也是原始论文中使用的,最为常用。
-
使用整个图像/特征空间的移动均值和方差,因此它们只是形状为
(n_channels,)
的向量(秩1)。
在两种情况下,你都会有:
输出 = gamma * (输入 - 均值) / 标准差 + beta
Beta通常设置为0,gamma设置为1,因为在BN之后有线性函数。
在训练过程中,均值
和方差
是在当前批次中计算的,当批次大小为1时会导致问题:
- 在第一种情况下,你会得到
均值=输入
,因此输出=0
- 在第二种情况下,
均值
将是所有像素的平均值,这样会好一些;但如果你的宽度和高度也为1,那么你再次得到均值=输入
,因此输出=0
。
我认为大多数人(以及原始方法)使用第一种方式,这就是为什么你会得到0(尽管TensorFlow文档似乎也建议第二种方法是常用的)。你提供的链接中的论点似乎是在考虑第二种方法。
无论你使用哪种方法,使用BN时,只有在使用较大的批量大小(例如,至少10)时,你才会得到好的结果。