关于人工智能:技术博客ResNet的介绍和实现

39次阅读

共计 5862 个字符,预计需要花费 15 分钟才能阅读完成。

ResNet 的介绍和实现

ResNet 的介绍

为什么要用 ResNet

 咱们都晓得:在训练卷积神经网络的过程中,当浅层的神经网络训练成果较差时,能够通过适当地加深网络的层数,从而获取一个优化成果更好的模型。这是因为随着网络的深度的减少,网络所能提取的信息就能更加的丰盛。然而在理论的试验过程中,咱们会发现:随着网络深度的加深,训练集的 loss 首先会逐步降落,而后趋于平缓;当咱们持续加深网络的深度时,训练集的 loss 反而开始回升。也就是说,网络呈现了“进化”(degradation)的景象。


图 1 20 层和 56 层的神经网络对 CIFAR-10 的训练后果图 [1]

 为了可能让深层的网络训练进去的模型成果优于浅层的网络,何凯明等人提出了一种新的网络结构,这就是 ResNet。

ResNet 的根本单元

 在解释 ResNet 是如何解决“进化”景象之前,先简略地介绍一下残差网络的根本单元,也就是残差块:


图 2 ResNet 的根本单元 [1]

ResNet 的残差块在一个或者多个卷积层之间加上一条“短接线”(shortcut connection),这条“短接线”会起到一个恒等映射(identity mapping)的作用。接下来认真介绍一下,以这种构造组成的 ResNet 网络为什么能够防止深层网络的“进化”景象:假如输出为 X,将以 X 为输出的某网络层的输入设为 H(X)。在个别的卷积神经网络如 VGG 中,由输出 X,须要通过网络层的训练间接拟合失去输入 H(X);而在上图所示的根本单元中,因为存在短接线,由输出 X,为失去输入 H(X),只须要通过网络层的训练拟合失去残差函数 F(X)=H(X)-X,再间接失去根本单元的输入 H(X)=F(X)+X。那么,为什么咱们要拟合残差函数 F(X)=H(X)-X,而不是间接拟合失去 H(X) 呢? 换句话说,拟合残差函数 F(X)=H(X)-X,与拟合 H(X) 相比,有什么益处呢?当初咱们假如网络达到某一深度的时候,该层的输入 X 曾经达到最优状态,也就是说,此时的错误率是最低的时候,再持续加深网络的层数就会呈现进化的景象。如果应用个别的卷积神经网络如 VGG,当初将该层的输入 X 作为下一层的输出进行训练,这时更新下一层的权值的代价绝对较高,因为下一层的权值要使得输入 H(X) 依然放弃最佳状态,即 H(X)=X。然而采纳 ResNet 时,还是假如网络达到某一深度的时候,该网络的输入 X 曾经达到最优状态,将该层的输入 X 作为下一层的输出进行训练,为了保障下一层的输入 H(X) 依然是最优状态,只须要拟合残差函数 F(x)=H(X)-X= 0 就能够了,这时下一层的输入 H(X) 就等于 X。当然下面提到的只是现实状况,然而总会有那么一个时刻,某一层的输入可能有限靠近最优解。所以,绝对于应用个别的卷积神经网络如 VGG,须要间接拟合失去输入 H(X),而采纳 ResNet 只须要拟合残差函数 F(x),而失去残差函数 F(x) 只须要更新 F(x) 少部分的权值就能够了。这就是 ResNet 的根本单元,以及为什么以残差块形成的 ResNet 能够防止深层网络的“进化”问题的起因。接下来,我介绍一下如何基于 tensorflow 形成残差块,并以残差块为根底搭建简略的 ResNet,并基于 CIFAR-10 图像集进行图像分类的训练。

ResNet 的实现

解决数据集

 在本文中,搭建了一个简略的 18 层的 ResNet[2],并基于 CIFAR-10 图像集进行图像分类的训练。这里,首先介绍一下 CIFAR-10 图像集的预处理的过程。
# 图像标准化解决,能够减少模型的泛化能力
img_mean = tf.constant([0.485, 0.456, 0.406])
img_std = tf.constant([0.229, 0.224, 0.225])
def normalize(x, mean=img_mean, std=img_std):
    x = (x - mean)/std
    return x

# 图像的预处理
def preprocess(x, y):
    x = tf.image.random_flip_left_right(x) # 图像增强:图像随机地左右翻转
    x = tf.cast(x, dtype=tf.float32) / 255. # [0,255]->[0,1]
    x = normalize(x) # 标准化解决
    y = tf.cast(y, dtype=tf.int32)
    return x, y

# 载入数据并进行预处理
(x, y), (x_val, y_val) = datasets.cifar10.load_data() # 载入 CIFAR-10 图像集
y = tf.squeeze(y) # 压缩张量为 1 的轴
y_val = tf.squeeze(y_val)
y = tf.one_hot(y, depth=10) # 将标签转化为 one-hot 模式
y_val = tf.one_hot(y_val, depth=10)
train_db = tf.data.Dataset.from_tensor_slices((x,y)) # 转化为 tensor 不便进一步解决
train_db = train_db.map(preprocess).shuffle(10000).batch(256) # 对训练集数据进行预处理,batchsize=256
test_db = tf.data.Dataset.from_tensor_slices((x_val, y_val))
test_db = test_db.map(preprocess).batch(256)
sample = next(iter(train_db))

搭建神经网络

 在本文中,通过 BasicBlock 类和 ResNet 类,搭建了一个如下构造的 18 层的 ResNet。


图 3 红框内即所搭建的 18 层 ResNet 的构造 [1]

# 残差块,即 ResNet 的根本单元
class BasicBlock(layers.Layer): # 次要是由两个卷积层和一条短接线组成
    def __init__(self, filter_num, stride=1): # filter_num:卷积层通道数,stride:步长
        super(BasicBlock, self).__init__()
        self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')# 卷积层 1
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu') #激活函数为 relu

        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')# 卷积层 2
        self.bn2 = layers.BatchNormalization()

        if stride != 1: # stride!= 1 须要下采样
            self.downsample = Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x:x

    def call(self, inputs, training=None): # 前向流传
        out = self.conv1(inputs)
        out = self.bn1(out, training=training)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out, training=training)
        identity = self.downsample(inputs)
        output = layers.add([out, identity])
        output = tf.nn.relu(output)
        return output


class ResNet(keras.Model): # 构建 ResNet 网络
    # 若 layer_dims=[2, 2, 2, 2],则生成 4 个 resblock,每个 resblock 有 2 个残差块;num_class 是分成类别的数目
    def __init__(self, layer_dims, num_classes=10): 
        super(ResNet, self).__init__()
        self.stem = Sequential([layers.Conv2D(64, (3, 3), strides=(1, 1)),    # 数据预处理层
                                layers.BatchNormalization(),
                                layers.Activation('relu'),
                                layers.MaxPool2D(pool_size=(2, 2), strides=(1, 1), padding='same')
                                ])
        # 生成 4 个 resblock,每个 resblock 的卷积层的通道数别离为 64,128,256,512
        self.layer1 = self.build_resblock(64,  layer_dims[0])
        self.layer2 = self.build_resblock(128, layer_dims[1], stride=2) # stride= 2 使得 h 和 w 逐步变小,有降维的性能
        self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
        self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)
        # output: [b, 512, h, w],h 和 w 未知
        self.avgpool = layers.GlobalAveragePooling2D() # 失去 [512,1,1], 即在 h 和 w 上求均匀
        self.fc1 = layers.Dense(num_classes, activation=tf.nn.softmax) # 全连贯层,进行分类输入

    def call(self, inputs, training=None):
        x = self.stem(inputs,training=training)
        x = self.layer1(x,training=training)
        x = self.layer2(x,training=training)
        x = self.layer3(x,training=training)
        x = self.layer4(x,training=training)
        x = self.avgpool(x)
        x = self.fc1(x)
        return x

    def build_resblock(self, filter_num, blocks, stride=1): # 构建一个 build_resblock,filter_num 是残差块中卷积层的通道数,blocks 是残差块的数量
        res_blocks = Sequential()
        res_blocks.add(BasicBlock(filter_num, stride)) # 第一个残差块可能须要下采样
        for _ in range(1, blocks): # 后续的残差块不须要下采样的能力
            res_blocks.add(BasicBlock(filter_num, stride=1))
        return res_blocks
    
    
# 搭建一个 ResNet
model = ResNet([2, 2, 2, 2])

编译模型

 应用了罕用的 Adam 优化器,以及 CategoricalCrossentropy 作为损失函数。
model.compile(optimizer=optimizers.Adam(lr=1e-3),
              loss=tf.losses.CategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

训练模型

h = model.fit(train_db, epochs=500, validation_data=test_db, validation_freq=1)

可视化后果

 模型训练过程中的数据会寄存在 h 中,为了更好地察看迭代过程,将其可视化输入。
def plot_acc_loss(h, nb_epoch):
    acc, loss, val_acc, val_loss = h.history['accuracy'], h.history['loss'], h.history['val_accuracy'], h.history['val_loss']
    plt.figure(figsize=(15, 5))
    plt.subplot(121)
    plt.plot(range(nb_epoch), acc, label='Train')
    plt.plot(range(nb_epoch), val_acc, label='Test')
    plt.title('Accuracy over' + str(nb_epoch) + 'Epochs', size=15)
    plt.legend()
    plt.grid(True)
    plt.subplot(122)
    plt.plot(range(nb_epoch), loss, label='Train')
    plt.plot(range(nb_epoch), val_loss, label='Test')
    plt.title('Loss over' + str(nb_epoch) + 'Epochs', size=15)
    plt.legend()
    plt.grid(True)
    plt.show()
    
    
plot_acc_loss(h, epochs_num) # 将损失函数和精确度随着 epoch 减少的变化趋势进行可视化 

后果如下图所示:

图 4 模型训练过程精确度和 loss 的可视化图像

从图中的训练后果能够看到,随着迭数的减少,训练集准确率逐步减少,当迭代次数超过 400 次后,趋向于稳固,证实模型的收敛性良好,在验证集上的精度能够靠近 90%,阐明 18 层 ResNet 的成果良好,模型的泛化能力不错。

Reference:

[1] He K , Zhang X , Ren S , et al. Deep Residual Learning for Image Recognition[J]. 2015.


[2] 代码起源:[《深度学习与 TensorFlow 2 入门实战》](https://study.163.com/course/courseMain.htm?share=1&shareId=1425444040&courseId=1209092816&_trace_c_p_k2_=b1289bf724a645d1a77db8a4e7c35a64)

正文完
 0