CNN(六):ResNeXt-50实战

  •   本文为365天深度学习训练营中的学习记录博客
  • 原作者:K同学啊|接辅导、项目定制

        ResNeXt是有何凯明团队在2017年CVPR会议上提出来的新型图像分类网络。它是ResNet的升级版,在ResNet的基础上,引入了cardinality的概念,类似于ResNet。ResNeXt也有ResNeXt-50,ResNeXt-101版本。

1 模型结构

        在ResNeXt的论文中,作者提出了当时普遍存在的一个问题,如果要提高模型的准确率,往往采取加深网络或者加宽网络的方法。虽然这种方法有效,但随之而来的,是网络设计的难度和计算开销的增加。为了一点精度的提升往往需要付出更大的代价,因此需要一个更好的策略,在不额外增加计算代价的情况下,提升网络的精度。由此,何等人提出了cardinality的概念。

        下面是ResNet(左)与ResNeXt(右)block的差异。在ResNet中,输入的具有256个通道的特征经过1x1卷积压缩4倍到64个通道,之后3x3的卷积核用于处理特征,经1x1卷积扩大通道数与原特征残差连接后输出。ResNeXt也是相同的处理策略,但在ResNeXt中,输入的具有256个通道的特征被分为32个组,每组被压缩64倍到4个通道后进行处理。32个组相加后与原特征残差连接后输出。这里cardinatity指的是一个block中所具有的相同分支的数目。

CNN(六):ResNeXt-50实战_第1张图片 图1 ResNet和ResNeXt​​​​

2 分组卷积 

        ResNeXt中采用的分组卷积简单来说是将特征图分为不同的组,再对每组特征图分别进行卷积,这个操作可以有效的降低计算量。

        在分组卷积中,每个卷积核只处理部分通道,比如下图中,红色卷积核只处理红色的通道,绿色卷积核只处理绿色的通道,黄色卷积核只处理黄色通道。此时每个卷积核有2个通道,每个卷积核生成一张特征图。

CNN(六):ResNeXt-50实战_第2张图片 图2 分组卷积示意图

        分组卷积的优势在于其参数开销,图3是其对比效果。

CNN(六):ResNeXt-50实战_第3张图片 图3 标准卷积和分组卷积参数量对比

 3 代码实现

3.1 开发环境

电脑系统:ubuntu16.04

编译器:Jupter Lab

语言环境:Python 3.7

深度学习环境:tensorflow

 3.2 前期准备

3.2.1 设置GPU

import tensorflow as tf
 
gpus = tf.config.list_physical_devices("GPU")
 
if gpus:
    tf.config.experimental.set_memory_growth(gpus[0], True) # 设置GPU显存用量按需使用
    tf.config.set_visible_devices([gpus[0]], "GPU")

3.2.2 导入数据

import matplotlib.pyplot as plt
# 支持中文
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
 
import os, PIL, pathlib
import numpy as np
 
from tensorflow import keras
from tensorflow.keras import layers,models
 
data_dir = "../data/bird_photos"
data_dir = pathlib.Path(data_dir)
 
image_count = len(list(data_dir.glob('*/*')))
print("图片总数为:", image_count)

3.2.3 加载数据

batch_size = 8
img_height = 224
img_width = 224
 
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset="training",
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size)
 
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset="validation",
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size)
 
class_Names = train_ds.class_names
print("class_Names:",class_Names)

3.2.4 可视化数据

plt.figure(figsize=(10, 5)) # 图形的宽为10,高为5
plt.suptitle("imshow data")
 
for images,labels in train_ds.take(1):
    for i in range(8):
        ax = plt.subplot(2, 4, i+1)
        plt.imshow(images[i].numpy().astype("uint8"))
        plt.title(class_Names[labels[i]])
        plt.axis("off")

3.2.5 检查数据

for image_batch, lables_batch in train_ds:
    print(image_batch.shape)
    print(lables_batch.shape)
    break

3.2.6 配置数据集

AUTOTUNE = tf.data.AUTOTUNE
 
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

3.3 ResNeXt-50代码实现

3.3.1 分组卷积模块

import numpy as np
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout, Conv2D, MaxPool2D, Flatten, GlobalAvgPool2D, concatenate, \
BatchNormalization, Activation, Add, ZeroPadding2D, Lambda
from tensorflow.keras.layers import ReLU
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import LearningRateScheduler
from tensorflow.keras.models import Model

# 定义分组卷积
def grouped_convolution_block(init_x, strides, groups, g_channels):
    group_list = []
    # 分组进行卷积
    for c in range(groups):
        # 分组取出数据
        x = Lambda(lambda x: x[:, :, :, c*g_channels:(c+1)*g_channels])(init_x)
        # 分组进行卷积
        x = Conv2D(filters=g_channels, kernel_size=(3,3), strides=strides, padding='same', use_bias=False)(x)
        # 存入list
        group_list.append(x)
    # 合并list中的数据
    group_merge = concatenate(group_list, axis=3)
    x = BatchNormalization(epsilon=1.001e-5)(group_merge)
    x = ReLU()(x)
    return x

3.3.2 残差单元

# 定义残差单元
def block(x, filters, strides=1, groups=32, conv_shortcut=True):
    if conv_shortcut:
        shortcut = Conv2D(filters*2, kernel_size=(1,1), strides=strides, padding='same', use_bias=False)(x)
        # epsilon位BN公式中防止分母为零的值
        shortcut = BatchNormalization(epsilon=1.001e-5)(shortcut)
    else:
        # identity_shortcut
        shortcut = x
        
    # 三层卷积层
    x = Conv2D(filters=filters, kernel_size=(1,1), strides=1, padding='same', use_bias=False)(x)
    x = BatchNormalization(epsilon=1.001e-5)(x)
    x = ReLU()(x)
    
    # 计算每组的通道数
    g_channels = int(filters / groups)
    # 进行分组卷积
    x = grouped_convolution_block(x, strides, groups, g_channels)
    
    x = Conv2D(filters=filters * 2, kernel_size=(1,1), strides=1, padding='same', use_bias=False)(x)
    x = BatchNormalization(epsilon=1.001e-5)(x)
    x = Add()([x, shortcut])
    x = ReLU()(x)
    
    return x

3.3.3 堆叠残差单元

        每个stack的第一个block的输入和输出shape是不一致的,所以残差连接都需要使用1x1卷积升维后才能进行Add操作。而其他block的输入和输出的shape是一致的,所以可以直接执行Add操作。

# 堆叠残差单元
def stack(x, filters, blocks, strides, groups=32):
    # 每个stack的第一个block的残差连接都需要使用1*1卷积升维
    x = block(x, filters, strides=strides, groups=groups)
    for i in range(blocks):
        x = block(x, filters, groups=groups, conv_shortcut=False)
    return x

3.3.4 搭建ResNeXt-50网络

# 定义ResNext50(32*4d)网络
def ResNext50(input_shape, num_classes):
    inputs = Input(shape=input_shape)
    # 填充3圈0,[224, 224, 3] -> [230, 230, 3]
    x = ZeroPadding2D((3,3))(inputs)
    x = Conv2D(filters=64, kernel_size=(7,7), strides=2, padding='valid')(x)
    x = BatchNormalization(epsilon=1.001e-5)(x)
    x = ReLU()(x)
    
    # 填充1圈0
    x = ZeroPadding2D((1, 1))(x)
    x = MaxPool2D(pool_size=(3,3), strides=2, padding='valid')(x)
    # 堆叠残差结构
    x = stack(x, filters=128, blocks=2, strides=1)
    x = stack(x, filters=256, blocks=3, strides=2)
    x = stack(x, filters=512, blocks=5, strides=2)
    x = stack(x, filters=1024, blocks=2, strides=2)
    # 根据特征图大小进行全局平均池化
    x = GlobalAvgPool2D()(x)
    x = Dense(num_classes, activation='softmax')(x)
    
    # 定义模型
    model = Model(inputs=inputs, outputs=x)
    return model

model = ResNext50(input_shape=(224,224,3), num_classes=4)
model.summary()

     结果显示如下(由于结果内容较多,只展示前后部分内容):

CNN(六):ResNeXt-50实战_第4张图片 (中间内容省略)

CNN(六):ResNeXt-50实战_第5张图片

3.4 正式训练

# 设置优化器
opt = tf.keras.optimizers.Adam(learning_rate=1e-4)
 
model.compile(optimizer="adam",
             loss='sparse_categorical_crossentropy',
             metrics=['accuracy'])
 
epochs = 10
 
history = model.fit(
                train_ds,
                validation_data=val_ds,
                epochs=epochs)

     结果如下图所示:

CNN(六):ResNeXt-50实战_第6张图片

3.5 模型评估

acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
 
loss = history.history['loss']
val_loss = history.history['val_loss']
 
epochs_range = range(epochs)
 
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.suptitle("ResNeXt-50 test")
 
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
 
plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation loss')
plt.legend(loc='upper right')
plt.title('Training and Validation loss')
plt.show()

      结果如下图所示: 

CNN(六):ResNeXt-50实战_第7张图片

4 总结

         总而言之,ResNeXt是在ResNet的网络架构上,使用类似于Inception的分治思想,即split-tranform-merge策略,将模块中的网络拆开分组,与Inception不同,每组的卷积核大小一致,这样其感受野一致,但由于每组的卷积核参数不同,提取的特征自然不同。然后将每组得到的特征进行concat操作后,再与原输入特征x或者经过卷积等处理(即进行非线性变换)的特征进行Add操作。这样做的好处是,在不增加参数复杂度的前提下提高准确率,同时还能提高超参数的数量。

        另外,cardinality是基的意思,将数个通道特征进行分组,不同的特征组之间可以看作是由不同基组成的子空间,每个组的核虽然一样,但参数不同,在各自的子空间中学到的特征就多种多样,这点跟transformer中的Multi-head attention不谋而合(Multi-head attention allows the model to jointly attend to information from different representation subspaces.)而且分组进行特征提取,使得学到的特征冗余度降低,获取能起到正则化的作用。(参考ResNeXt的分类效果为什么比Resnet好? - 知乎)

你可能感兴趣的:(CNN,cnn,人工智能,神经网络)