文章目录

  • 前言
  • ResNet原理
  • Basic Block
  • Res Block
  • CiFar100与ResNet18实战
  • ResNet18框架
  • ResNet18.py代码
  • 主文件run.py代码
  • CiFar100与ResNet34代码
  • 步长不为1,padding为same时
  • Python中的__init__()和__call__()函数
  • __ init __()
  • __ call __()


前言

神经网络的层数越堆越多,并不意味着效果越来越好

当模型加以后,网络变得越来越难训练,这主要是由于梯度弥散和梯度爆炸现象造成的。在较深层数的神经网络中,梯度信息由网络的末层逐层传向网络的首层时,传递的过程中会出现梯度接近于0或梯度值非常大的现象。网络层数越,这种现象可能会越严重。

那么怎么解决深层神经网络的梯度弥散和梯度爆炸现象呢?一个很自然的想法是,既然浅层神经网络不容易出现这些梯度现象,那么可以尝试给深层神经网络添加一种回退到浅层神经网络的机制。当深层神经网络可以轻松地回退到浅层神经网络时,深层神经网络可以获得与浅层神经网络相当的模型性能,而不至于更糟糕。

通过在输入和输出之间添加一条直接连接的Skip Connection可以让神经网络具有回退的能力。

以 VGG13 深度神经网络为例,假设观察到 VGG13 模型出现梯度弥散现象,而 10 层的网络模型并没有观测到梯度弥散现象,那么可以考虑在最后的两个卷积层添加 Skip Connection,如图 10.62 中所示。通过这种方式,网络模型可以自动选择是否经由这两个卷积层完成特征变换,还是直接跳过这两个卷积层而选择 Skip Connection,亦或结合两个卷积层和 Skip Connection 的输出。

ResNet各型号_人工智能

ResNet原理

ResNet通过在卷积层的输入和输出之间添加Skip Connection实现层数回退机制,如下图10.63所示,输入𝒙通过两个卷积层,得到特征变换后的输出F(𝒙),与输入𝒙进行对应元素的相加运算,得到最终输出H(𝒙): H(𝒙) = 𝒙 + F(𝒙)
H(𝒙)叫作残差模块(Residual Block,简称 ResBlock)由于被 Skip Connection 包围的卷积神经网络需要学习映射F(𝒙) = H(𝒙) − 𝒙,故称为残差网络。

为了能够满足输入𝒙与卷积层的输出F(𝒙)能够相加运算,需要输入𝒙的 shape 与F(𝒙)的 shape 完全一致。当出现 shape 不一致时,一般通过在 Skip Connection 上添加额外的卷积运算环节将输入𝒙变换到与F(𝒙)相同的 shape,如图 10.63 中identity(𝒙)函数所示,其中 identity(𝒙)以 1 × 1 的卷积运算居多,主要用于调整输入的通道数

ResNet各型号_神经网络_02


ResNet各型号_神经网络_03

Basic Block

ResNet各型号_ResNet各型号_04

Res Block

Res Block 由多个Basic Block构成

ResNet各型号_神经网络_05

CiFar100与ResNet18实战

数据传入ResNet18框架后,到所有Sequential()里逛一圈出来

ResNet18框架

最下面是fc100:

ResNet各型号_ResNet各型号_06

ResNet18.py代码

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential

# 继承自layers.Layer里面不能有容器
class BasicBlock(layers.Layer):
    # 残差模块
    def __init__(self, filter_num, stride=1):
        super(BasicBlock, self).__init__()
        # 第一个卷积单元
        self.conv1 = layers.Conv2D(filter_num, (3,3), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization() # BN层只修改channel通道
        self.relu = layers.Activation('relu')
        # 第二个卷积单元
        self.conv2 = layers.Conv2D(filter_num, (3,3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization() # BN层只修改channel通道

        if stride != 1: # 用于修改输入的维度,使其与BasicBlock的输出维度相同,方便做加法
            #self.downsample = Sequential()
            #self.downsample.add(layers.Conv2D(filter_num, (3,3), strides=stride, padding='same'))
            #self.downsample.add(layers.BatchNormalization())
            self.downsample = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
        else:
            self.downsample = lambda x:x

    # 前向传播, 让输入x进去BasicBlock逛一圈出来得到F(x)
    # inputs的shape是[b, h, w, c];
    # 将training设置为None,表示不进行梯度更新;
    def call(self, inputs, training=None):
        # [b, h, w, c], 通过第一个卷积单元
        out = self.conv1(inputs) # 等于self.conv1.call(inputs)
        out = self.bn1(out) # 等于self.bn1.call(out)
        out = self.relu(out) # 等于self.relu.call(out)
        # 通过第二个卷积单元
        out = self.conv2(out) # 等于self.bn1.call(out)
        output = self.bn2(out) # 等于self.bn2.call(out), output相当于F(x)
        # 通过identity模型
        identify = self.downsample(inputs) # 修改inputs的维度,和output的维度相同
        # 2条路径输出直接相加
        output = layers.add([output, identify])
        output = tf.nn.relu(output) # 激活函数
        return output

# BasicBlock 并不是处理的最终单元, 多个BasicBlock组成的ResBlock才是大的模块
# 通用的 ResNet 实现类
# 继承自keras.Model里面可以有容器
class ResNet(keras.Model):
    # layers_dims=[2,2,2,2],这个"列表"里面每个元素代表每次调用"函数build_resblock"时,添加几个BasicBlock;
    # [2,2,2,2]说明有4块ResBlock,每块里面有2个BasicBlock
    # num_classes是标签数量,这里使用"CIFAR100数据集"就有100个标签
    def __init__(self, layers_dims, num_classes=100):
        super(ResNet, self).__init__()

        # 预处理单元
        self.pre = Sequential([
            layers.Conv2D(64, (3, 3), strides=(1,1), padding='same'), # 卷积层是把高h/宽w转变到通道c
            layers.BatchNormalization(), # BN层只动通道c,不动高h/宽w
            layers.Activation('relu'),
            layers.MaxPool2D(pool_size=(2,2), strides=(1,1), padding='same')# 池化层也改变高h/宽w,不动通道c
        ])
        # 堆叠 4 个 Block,每个 block 包含了多个 BasicBlock,设置步长不一样
        self.layer1 = self.build_resblock(64, layers_dims[0])
        self.layer2 = self.build_resblock(128, layers_dims[1], stride=2)
        self.layer3 = self.build_resblock(256, layers_dims[2], stride=2)
        self.layer4 = self.build_resblock(512, layers_dims[3], stride=2)

        # 经过上面四层以后, 假设目前的输出维度为 output:[b, h, w, 512]
        # 接着,通过 Pooling 层将高宽降低为 1x1,维度变为: [b, 1, 1, 512]
        self.avgpool = layers.GlobalAveragePooling2D()
        self.fc = layers.Dense(num_classes) # 经过全连接层变为[b, 100]

    # 只想测试一下当前模型,不需要反向求导,赋值training=None,则只会给出当前模型的预测结果;
    # 如果输入的数据是用来训练的,需要进行梯度更新,则赋值training=True
    def call(self, inputs, training=None):
        x = self.pre(inputs) # [b, 32, 32, 64]
        # 一次通过4个模块
        x = self.layer1(x) # [b, 32, 32, 64]
        x = self.layer2(x) # [b, 16, 16, 128]
        x = self.layer3(x) # [b, 8, 8, 256]
        x = self.layer4(x) # [b, 4, 4, 512]
        # 通过池化层
        x = self.avgpool(x) # [b, 1, 1, 512]
        # 通过全连接层
        out = self.fc(x) # 此时的输出应该为 [b, 100]

        return out


    def build_resblock(self, filter_num, blocks, stride=1):
        # 通过build_resblock函数构建了一个Sequential()容器;
        # 这个Sequential()容器里根据你指定的块数blocks,在容器里添加了若干的basicblock,每个basicblock在内部有5层;
        # 假设参数blocks=3,那么就是一共有3个basicblock,每个basicblock里面有5层
        res_blocks = Sequential()
        # 只有第一个BasicBlock的步长可能不为1,实现下采样
        res_blocks.add(BasicBlock(filter_num, stride))

        for _ in range(1, blocks): # 其他BasicBlock步长都为1
            res_blocks.add(BasicBlock(filter_num, stride=1))

        return res_blocks

def resNet18():
    # 通过调整模型内部BasicBlock的数量和配置实现不同的ResNet
    return ResNet([2,2,2,2], 100)

def resNet34():
    # 通过调整模型内部BasicBlock的数量和配置实现不同的ResNet
    return ResNet([3,4,6,3], 100)

主文件run.py代码

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, optimizers, datasets, Sequential
import os
from ResNet18 import resNet18


os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'

# Step1 数据准备
(x,y), (x_test, y_test) = datasets.cifar100.load_data()
y = tf.squeeze(y)
y_test = tf.squeeze(y_test)
print('x, y', x.shape, y.shape)
print('x_test, y_test', x_test.shape, y_test.shape)

def preprocessing(input_x, input_y):
    input_x = tf.cast(input_x, dtype=tf.float32) / 255.
    input_y = tf.cast(input_y, dtype=tf.int32)
    
    return input_x, input_y


train_db = tf.data.Dataset.from_tensor_slices((x, y))
train_db = train_db.shuffle(1000).map(preprocessing).batch(128)

train_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
train_test = train_test.map(preprocessing).batch(128)

sample = next(iter(train_db))
print('train_sample:', sample[0].shape)
print('train_sample[1]', sample[1].shape)
print('train_sample的长度', len(sample))

sample = next(iter(train_test))
print('test_sample:', sample[0].shape)
print('test_sample[1]', sample[1].shape)
print('testsample的长度', len(sample))

# Step2 Model
# Step3 Train


def train():
    model = resNet18()
    model.build(input_shape=(None, 32, 32, 3))
    model.summary()

    optimizer = optimizers.Adam(learning_rate=1e-4)

    for epoch in range(40):


        for step, (x,y) in enumerate(train_db):

            with tf.GradientTape() as tape:
                logits = model(x)

                y_onehot = tf.one_hot(y, depth=100)

                # 计算loss
                loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
                loss = tf.reduce_mean(loss)

            grads = tape.gradient(loss, model.trainable_variables)
            optimizer.apply_gradients(zip(grads, model.trainable_variables))

            if step % 100 == 0:
                print('epoch', epoch, 'step:', step)

        total_number = 0
        total_correct = 0

        for x_test, y_test in train_test:

            logits = model(x_test)

            probility = tf.nn.softmax(logits, axis=1)
            pred = tf.argmax(probility, axis=1)

            pred = tf.cast(pred, dtype=tf.int32)

            correct = tf.cast(tf.equal(pred, y_test), dtype=tf.int32)
            correct = tf.reduce_sum(correct)

            total_correct += int(correct)
            total_number += x_test.shape[0]

        acc = total_correct / total_number
        print('total_correct:', total_correct,'total_number:', total_number)
        print('acc:', acc)

train()

CiFar100与ResNet34代码

链接: https://pan.baidu.com/s/13YNLG57mEOO1QjgfivEjvQ?pwd=pb4c 提取码: pb4c

ResNet34框架图:(注:最后全连接层输出应是100)

ResNet各型号_深度学习_07

步长不为1,padding为same时

当步长s>1时,设置padding=‘same’,将使得输出高、宽成1/s倍减少。高宽先padding成可以整除s的最小整数,再除以步长:

例1:

ResNet各型号_人工智能_08


例2:

ResNet各型号_tensorflow_09


例3:

ResNet各型号_ResNet各型号_10


例4:

ResNet各型号_神经网络_11


例5:

ResNet各型号_神经网络_12

Python中的__init__()和__call__()函数

在Python的class中有一些函数往往具有特殊的意义。__init__()__call__()就是class很有用的两类特殊的函数

__ init __()

__init__()方法的作用是初始化一个类的实例

__ call __()

call方法使得Python中类的实例(对象)可以被当做函数对待。 也就是说,我们可以将它们作为输入传递到其他的函数/方法中并调用他们,正如我们调用一个正常的函数那样。而类中__call__()函数的意义正在于此。为了将一个类实例当做函数调用,我们需要在类中实现__call__()方法。也就是我们要在类中实现如下方法:def call(self, *args)。这个方法接受一定数量的变量作为输入。
假设myCar是类的一个实例。那么调用myCar.__call__(1,2)等同于调用myCar(1,2)。这个实例本身在这里相当于一个函数。

总结
那么,__ init __() 和 __ call __()的区别如下:

__ init __()的作用是初始化某个类的一个实例;
__ call __()的作用是使实例能够像函数一样被调用。

ResNet各型号_人工智能_13