tensorflow2.0训练网络的问题(包括BatchNormalization以及Dropout)

近期准备做一些关于深度学习图像篇的教程,主要包括分类网络,目标检测网络、图像分割网络,并以pytorch1.3以及tensorflow2.0分别去搭建实现。近期使用tensorflow2.0训练网络时遇到了很多问题,在这简单做个总结。

使用环境:Python3.6(Anaconda管理)、Tensorflow2.0.0rc1

 

1.到底使用subclassed API还是使用官方推荐的keras functional API

具体使用哪种方法,主要还是看使用者的习惯,看你喜欢使用像pytorch那样的subclassed搭建模型方式,还是喜欢使用tensorflow原来的搭建模型方式,不过根据官方教程来看,官方还是更推荐使用keras functional API的方式。如果你使用subclassed模式,这里有个坑,实例化后需要手动build一下,否则无法使用summary函数查看网络结构,最坑的是使用迁移学习时如果不提前手动build是无法载入预训练权重的,并且程序也不会报错,小心踩坑。个人建议还是使用keras functional API,坑要少点。

subclassed搭建模型方式(搭建alexNet为例)

from tensorflow.keras import layers, models, Model, Sequential
class AlexNet(Model):
    def __init__(self, class_num=1000):
        super(AlexNet, self).__init__()
        self.features = Sequential([                                                # input(None, 224, 224, 3)
            layers.ZeroPadding2D(((1, 2), (1, 2))),                                 # output(None, 227, 227, 3)
            layers.Conv2D(48, kernel_size=11, strides=4, activation="relu"),        # output(None, 55, 55, 48)
            layers.MaxPool2D(pool_size=3, strides=2),                               # output(None, 27, 27, 48)
            layers.Conv2D(128, kernel_size=5, padding="same", activation="relu"),   # output(None, 27, 27, 128)
            layers.MaxPool2D(pool_size=3, strides=2),                               # output(None, 13, 13, 128)
            layers.Conv2D(192, kernel_size=3, padding="same", activation="relu"),   # output(None, 13, 13, 192)
            layers.Conv2D(192, kernel_size=3, padding="same", activation="relu"),   # output(None, 13, 13, 192)
            layers.Conv2D(128, kernel_size=3, padding="same", activation="relu"),   # output(None, 13, 13, 128)
            layers.MaxPool2D(pool_size=3, strides=2)])                              # output(None, 6, 6, 128)

        self.flatten = layers.Flatten()
        self.classifier = Sequential([
            layers.Dropout(0.2),
            layers.Dense(2048, activation="relu"),                                  # output(None, 2048)
            layers.Dropout(0.2),
            layers.Dense(2048, activation="relu"),                                   # output(None, 2048)
            layers.Dense(class_num),                                                # output(None, 5)
            layers.Softmax()
        ])

    def call(self, inputs):
        x = self.features(inputs)
        x = self.flatten(x)
        x = self.classifier(x)
        return x

 

keras functional API搭建模型方式(搭建alexNet为例)

from tensorflow.keras import layers, models, Model, Sequential
def AlexNet(im_height=224, im_width=224, class_num=1000):
    # tensorflow中的tensor通道排序是NHWC                                          
    input_image = layers.Input(shape=(im_height, im_width, 3), dtype="float32")  # output(None, 224, 224, 3)
    x = layers.ZeroPadding2D(((1, 2), (1, 2)))(input_image)                      # output(None, 227, 227, 3)
    x = layers.Conv2D(48, kernel_size=11, strides=4, activation="relu")(x)       # output(None, 55, 55, 48)
    x = layers.MaxPool2D(pool_size=3, strides=2)(x)                              # output(None, 27, 27, 48)
    x = layers.Conv2D(128, kernel_size=5, padding="same", activation="relu")(x)  # output(None, 27, 27, 128)
    x = layers.MaxPool2D(pool_size=3, strides=2)(x)                              # output(None, 13, 13, 128)
    x = layers.Conv2D(192, kernel_size=3, padding="same", activation="relu")(x)  # output(None, 13, 13, 192)
    x = layers.Conv2D(192, kernel_size=3, padding="same", activation="relu")(x)  # output(None, 13, 13, 192)
    x = layers.Conv2D(128, kernel_size=3, padding="same", activation="relu")(x)  # output(None, 13, 13, 128)
    x = layers.MaxPool2D(pool_size=3, strides=2)(x)                              # output(None, 6, 6, 128)

    x = layers.Flatten()(x)                         # output(None, 6*6*128)
    x = layers.Dropout(0.2)(x)
    x = layers.Dense(2048, activation="relu")(x)    # output(None, 2048)
    x = layers.Dropout(0.2)(x)
    x = layers.Dense(2048, activation="relu")(x)    # output(None, 2048)
    x = layers.Dense(class_num)(x)                  # output(None, 5)
    predict = layers.Softmax()(x)

    model = models.Model(inputs=input_image, outputs=predict)
    return model

 

2.训练过程中使用tf.keras中的fit_generator方法还是使用低层api方法去训练

首先说下,在官方提供的一些教程中,很多例子都是使用fit方法训练,其实在真正应用中一般很少用fit方法而是使用fit_generator的方法,因为fit方法是一次性将所有样本载入内存,而fit_generator方法是分批载入内存的。在训练自己的图像样本时一般不可能一次性载入内存(内存不够),所以fit_generator的方法更常用。

使用tf.keras中的fit系列方法的好处就是训练更简单,对很底层的东西你都不需要去管,但缺点在于封装程度太高可控性差。

使用低层api的方法好处在于用户能够精确去控制训练的流程,缺点在于代码量多一点,而且需要你注意BN,Dropout层的状态(这些层在train模式以及validation模式下设置是不同的,这里坑比较多,使用fit系列方法可以不用去管)

除了以上差异外还发现使用低层的api在使用迁移学习的过程中速度明显更快,速度大概是fit系列方法的2倍以上(在CPU上跑是这样,GPU上还没测试,没有查看源码暂不知道原因),我个人推荐使用低层api的方法,能够自主控制训练过程,fit方法有些诡异。

常用fit_generator的形式如下(部分代码):

model = MyNet(im_height=im_height, im_width=im_width, class_num=5)
model.summary()

# using keras high level api for training
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
              loss=tf.keras.losses.CategoricalCrossentropy(from_logits=False),
              metrics=["accuracy"])

callbacks = [tf.keras.callbacks.ModelCheckpoint(
    filepath='./save_weights/myNet_{epoch}.h5',
    save_best_only=True,
    save_weights_only=True,
    monitor='val_loss')]

model.fit_generator(generator=train_data_gen,
                    steps_per_epoch=total_train // batch_size,
                    epochs=epochs,
                    validation_data=val_data_gen,
                    validation_steps=total_val // batch_size,
                    callbacks=callbacks)

其中train_data_gen和val_data_gen是生成器,用于预处理并生成训练、测试样本。可以看出使用fit系列方法很简单,但你无法精准控制训练的每个batch。

 

使用低层api的训练方法如下(部分代码):

# using keras low level api for training
loss_object = tf.keras.losses.CategoricalCrossentropy(from_logits=False)
optimizer = tf.keras.optimizers.Adam(learning_rate=0.0001)

train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.CategoricalAccuracy(name='train_accuracy')

test_loss = tf.keras.metrics.Mean(name='test_loss')
test_accuracy = tf.keras.metrics.CategoricalAccuracy(name='test_accuracy')


@tf.function
def train_step(images, labels):
    with tf.GradientTape() as tape:
        output = model(images, training=True)
        loss = loss_object(labels, output)
    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))

    train_loss(loss)
    train_accuracy(labels, output)


@tf.function
def test_step(images, labels):
    output = model(images, training=False)
    t_loss = loss_object(labels, output)

    test_loss(t_loss)
    test_accuracy(labels, output)


best_test_loss = float('inf')
for epoch in range(1, epochs+1):
    train_loss.reset_states()        # clear history info
    train_accuracy.reset_states()    # clear history info
    test_loss.reset_states()         # clear history info
    test_accuracy.reset_states()     # clear history info

    # train
    for step in range(total_train // batch_size):
        images, labels = next(train_data_gen)
        train_step(images, labels)

    # validate
    for step in range(total_val // batch_size):
        test_images, test_labels = next(val_data_gen)
        test_step(test_images, test_labels)

    template = 'Epoch {}, Loss: {}, Accuracy: {}, Test Loss: {}, Test Accuracy: {}'
    print(template.format(epoch,
                          train_loss.result(),
                          train_accuracy.result() * 100,
                          test_loss.result(),
                          test_accuracy.result() * 100))
    if test_loss.result() < best_test_loss:
        best_test_loss = test_loss.result()
        model.save_weights("./save_weights/myNet_{}.ckpt".format(epoch), save_format='tf')

从代码量上明显能够看到多了很多,但是能够精准控制训练流程。注意在每个epoch的开始都会对统计的loss以及accurate进行reset_status操作,如果不进行重置操作(即清除历史记录)会导致loss和accurate记录的不准即loss会偏大,accurate会偏低,这里要吐槽一下官方的例子,在官方的例子中并没有在每个epoch开始时清除历史记录,这会误导学习者。关于BN和Dropout的坑在下个问题中会讲到。

 

3.训练过程中BatchNormalization以及Dropout该如何处理

大家都知道Dropout和BN是解决过拟合非常常用的方法,特别是BN使用非常普遍,但如果使用不当很容易踩坑,特别是BatchNormalization(让人又爱又恨)。为什么dropout与bn层这么特殊,简单讲下原理。

dropout层常用在最后的全连接层之间,在正向传播过程中会随机以一定概率将部分节点的值置零,这样能减轻过拟合的情况

BN层常被用在conv卷积层与relu激活层之间,用来调整正向传播过程中卷积后特征层值的分布,可防止梯度消失或爆炸以减轻过拟合的情况

bn相比dropout更加特殊,因为bn中有四个学习参数,分别是moving_mean、moving_variance、gamma、bate(注意其中有些参数是在正向传播过程中学习更新的,坑就在于此)。

在训练过程的正向传播过程中我们希望(1)dropout随机置零节点(2)bn调节值的分布并根据样本训练自身参数

在测试推理的过程中(只存在正向传播)我们希望(1)dropout不对节点做任何操作(2)bn调节值的分布,不去学习更新自身参数

如果使用fit系列操作,我们不用去管以上说的各种问题,fit操作会自动帮我们处理,但如果使用低层的API,我们需要在train模式以及validation模式下分别传入不同的参数trainning来控制dropout以及BN的状态。其实也很简单,在train模式下传入training=True参数,在validation模式下传入training=False参数即可(model.trainable参数无法控制bn层状态):

@tf.function
def train_step(images, labels):
    with tf.GradientTape() as tape:
        output = model(images, training=True)
        loss = loss_object(labels, output)
    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))

    train_loss(loss)
    train_accuracy(labels, output)


@tf.function
def test_step(images, labels):
    output = model(images, training=False)
    t_loss = loss_object(labels, output)

    test_loss(t_loss)
    test_accuracy(labels, output)

 

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

太阳花的小绿豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值