基于DenseNet的图像识别

1、DenseNet简介

DenseNet的文章我以前写过,原理篇看这里:DenseNet:Densely Connected Convolutional Networks–CVPR2017最佳论文奖

image-20220823211920935

在本文中,我们提出了一种架构,将这种见解提炼成一个简单的连接模式:为了确保网络中各层之间的最大信息流,我们将所有层(具有匹配的特征图大小)直接相互连接。为了保持前馈特性,每一层都从所有前面的层获得额外的输入,并将其自己的特征图传递给所有后续层。图 1 示意性地说明了这种布局。至关重要的是,与 ResNets 相比,我们在将特征传递到层之前从不通过求和来组合特征。相反,我们通过连接(Concatenate操作)它们来组合特征

因此,第 ℓ 层有 ℓ 输入,由所有先前卷积块的特征图组成。它自己的特征图被传递到所有 L−ℓ 后续层。这在 L 层网络中引入了 L(L+1)2 个连接,而不是传统架构中的仅 L 个连接。由于其密集的连接模式,我们将我们的方法称为密集卷积网络(DenseNet)

2、网络结构

image-20220823214539490

表 1:ImageNet 的 DenseNet 架构。前 3 个网络的增长率为 k = 32,对于 DenseNet-161,k = 48。请注意,表中显示的每个“conv”层都对应于序列 BN-ReLU-Conv

3、花朵数据集

训练集1088张图片,17个类别 测试集272张图片,17个类别。 每个文件夹对应一个花朵类别。

image-20220914213832131

image-20220914213925038

4、DenseNet花朵识别

import tensorflow as tf
import numpy as np
from tensorflow.keras import layers
from tensorflow.keras.models import Model
from tensorflow.keras.applications import DenseNet121
import math
import os
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, \
    BatchNormalization, Activation, Add, ZeroPadding2D, Multiply,Conv1D,GlobalAveragePooling2D,Reshape,multiply,ReLU,MaxPooling2D,Concatenate,\
    AveragePooling2D
from tensorflow.keras.optimizers import Adam
import matplotlib.pyplot as plt
from tensorflow.keras.callbacks import LearningRateScheduler
from tensorflow.keras.models import Model

4.1 Dense Block

Dense Block的每一个密集层:B N + R e L U + 1 ∗ 1 C o n v + B N + R e L U + 3 ∗ 3 C o n v BN+ReLU+1*1Conv+BN+ReLU+3*3ConvBN+ReLU+11Conv+BN+ReLU+33Conv

每一层产生k个特征图,原论文中让每个1*1个卷积产生4k个特征图

growth_rate:网络的增长率,原论文中超参数名称为k

论文中对内核大小为3*3的卷积层,输入的每一边都用一个像素补零,以保持特征图大小固定。(代码中设置padding='same’即可)

# Bottleneck layers
# BN+ReLU+1*1Conv+BN+ReLU+3*3Conv
# 每一层产生k个特征图,原论文中让每个1*1个卷积产生4k个特征图
# growth_rate:网络的增长率,原论文中超参数名称为k
def conv_block(x,growth_rate,name):
    x1=BatchNormalization(name=name+'_0_bn')(x)
    x1=ReLU(name=name+'_0_relu')(x1)
    x1=Conv2D(filters=4*growth_rate,
              kernel_size=(1,1),
              use_bias=False,
              name=name+'_1_conv')(x1)
    x1=BatchNormalization(name=name+'_1_bn')(x1)
    x1=ReLU(name=name+'_1_relu')(x1)
    # 论文原话:对内核大小为3*3的卷积层,输入的每一边都用一个像素补零,以保持特征图大小固定。
    x1=Conv2D(growth_rate,
              kernel_size=(3,3),
              padding='same',
              use_bias=False,
              name=name+'_2_conv')(x1)
    # 将前面所有层的特征堆叠后传到下一层
    out=Concatenate(axis=-1,name=name+'_concat')([x,x1])
    return out

'''
x: input tensor.
    blocks: integer, the number of building blocks.
    name: string, block label.
'''
# DenseNet
def dense_block(x,blocks,name):
    for i in range(blocks):
        x=conv_block(x,32,name=name+'_block'+str(i+1))
    return x

4.2 Transition Layer(过渡层)

过渡层:B N + 1 ∗ 1 C o n v + 2 ∗ 2 A v g P o o l BN+1*1Conv+2*2AvgPoolBN+11Conv+22AvgPool

reduction:压缩因子,原论文中名称为θ \thetaθ,设置为0.5。

# 论文原话:使用1*1卷积和2*2平均池化作为两个连续密集块之间的过渡层
# 过渡层:BN+1*1Conv+2*2AvgPool
# reduction:压缩因子,原论文中设置为0.5
def transition_block(x,reduction,name):
    x=BatchNormalization(name=name+'_bn')(x)
    x=ReLU(name=name+'_relu')(x)
    x=Conv2D(filters=int(x.shape[-1]*reduction),
             kernel_size=(1,1),
             use_bias=False,
             name=name+'_conv')(x)
    x=AveragePooling2D(pool_size=(2,2),strides=2,name=name+'_pool')(x)
    return x

4.3 DensNet121网络骨干

DenseNet121(k=32):blocks=[6,12,24,16]
DenseNet169(k=32):blocks=[6,12,32,32]
DenseNet201(k=32):blocks=[6,12,48,32]
DenseNet161(k=48):blocks=[6,12,36,24]

def DenseNet(blocks,include_top=True,weights='imagenet',intput_shape=None,classes=1000):
    img_input=Input(shape=intput_shape)
    x=ZeroPadding2D(padding=((3,3),(3,3)))(img_input)
    # 论文原话:初始卷积层包含2k个大小为7*7的卷积,步长为2(k=32)
    # 其实这里完全可以去掉这两个ZeroPadding2D,直接给这里的卷积和池化设置padding='same'即可,
    # 但是我看谷歌的源码中没用same padding,所以这里我也没用。
    x=Conv2D(filters=64,kernel_size=(7,7),strides=2,use_bias=False,name='conv1/conv')(x)
    x=BatchNormalization(name='conv1/bn')(x)
    x=ReLU(name='conv1/relu')(x)
    x=ZeroPadding2D(padding=((1,1),(1,1)))(x)
    x=MaxPooling2D(pool_size=(3,3),strides=2,name='pool1')(x)

    x=dense_block(x,blocks[0],name='conv2')
    x=transition_block(x,0.5,name='pool2')

    x=dense_block(x,blocks[1],name='conv3')
    x=transition_block(x,0.5,name='pool3')

    x=dense_block(x,blocks[2],name='conv4')
    x=transition_block(x,0.5,name='pool4')

    x=dense_block(x,blocks[3],name='conv5')

    x=BatchNormalization(name='bn')(x)
    x=ReLU(name='relu')(x)
    if include_top:
        x=GlobalAveragePooling2D(name='global_avg_pool')(x)
        x=Dense(classes,activation='softmax')(x)

    model=Model(img_input,x)
    return model

densenet121=DenseNet(blocks=[6,12,24,16],include_top=False,intput_shape=(224,224,3))
densenet121.summary()

image-20220914214441979

# 类别数
num_classes = 17
# 批次大小
batch_size = 32
# 周期数
epochs=100
# 图片大小
image_size = 224

model=tf.keras.Sequential([
    densenet121,
    layers.GlobalAveragePooling2D(),
    layers.Dense(num_classes,activation='softmax')
])
model.summary()

image-20220914214510977

这里仿照源码搭建的,不太优雅,其实可以一次性直接搭建好的,这里懒得改了。

4.4 数据增强

# 训练集数据进行数据增强
train_datagen = ImageDataGenerator(
    rotation_range=20,  # 随机旋转度数
    width_shift_range=0.1,  # 随机水平平移
    height_shift_range=0.1,  # 随机竖直平移
    rescale=1 / 255,  # 数据归一化
    shear_range=10,  # 随机错切变换
    zoom_range=0.1,  # 随机放大
    horizontal_flip=True,  # 水平翻转
    brightness_range=(0.7, 1.3),  # 亮度变化
    fill_mode='nearest',  # 填充方式
)
# 测试集数据只需要归一化就可以
test_datagen = ImageDataGenerator(
    rescale=1 / 255,  # 数据归一化
)

# 训练集数据生成器,可以在训练时自动产生数据进行训练
# 从'data/train'获得训练集数据
# 获得数据后会把图片resize为image_size×image_size的大小
# generator每次会产生batch_size个数据
train_generator = train_datagen.flow_from_directory(
    '../data/花朵分类(17)/train',
    target_size=(image_size, image_size), # 调整图像尺寸
    batch_size=batch_size,
)

# 测试集数据生成器
test_generator = test_datagen.flow_from_directory(
    '../data/花朵分类(17)/test',
    target_size=(image_size, image_size),
    batch_size=batch_size,
)
# 字典的键为17个文件夹的名字,值为对应的分类编号
print(train_generator.class_indices)

image-20220914214622027

4.5 callback

# 学习率调节函数,逐渐减小学习率
def adjust_learning_rate(epoch):
    # 前40周期
    if epoch<=40:
        lr = 1e-4
    # 前40到80周期
    elif 40 < epoch <= 80:
        lr = 1e-5
    # 80到100周期
    else:
        lr = 1e-6
    return lr

# 定义优化器
adam = Adam(lr=1e-4)

# 读取模型
checkpoint_save_path = "./checkpoint/DenseNet121(不使用预训练)-花朵分类(17)-.ckpt"
if os.path.exists(checkpoint_save_path + '.index'):
    print('-------------load the model-----------------')
    model.load_weights(checkpoint_save_path)
# 保存模型
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
                                                 save_weights_only=True,
                                                 save_best_only=True)

# 定义学习率衰减策略
callbacks = []
callbacks.append(LearningRateScheduler(adjust_learning_rate))
callbacks.append(cp_callback)

4.6 模型训练

# 定义优化器,loss function,训练过程中计算准确率
model.compile(optimizer=adam,loss='categorical_crossentropy',metrics=['accuracy'])

# Tensorflow2.1版本(包括2.1)之后可以直接使用fit训练模型
history = model.fit(x=train_generator,epochs=epochs,validation_data=test_generator,callbacks=callbacks)

image-20220914214742021

这里没有使用预训练,所以效果没有官方的好。

4.7 acc和loss可视化

# 画出训练集准确率曲线图
plt.plot(np.arange(epochs),history.history['accuracy'],c='b',label='train_accuracy')
# 画出验证集准确率曲线图
plt.plot(np.arange(epochs),history.history['val_accuracy'],c='y',label='val_accuracy')
# 图例
plt.legend()
# x坐标描述
plt.xlabel('epochs')
# y坐标描述
plt.ylabel('accuracy')
# 显示图像
plt.show()

image-20220914214823267

# 画出训练集loss曲线图
plt.plot(np.arange(epochs),history.history['loss'],c='b',label='train_loss')
# 画出验证集loss曲线图
plt.plot(np.arange(epochs),history.history['val_loss'],c='y',label='val_loss')
# 图例
plt.legend()
# x坐标描述
plt.xlabel('epochs')
# y坐标描述
plt.ylabel('loss')
# 显示图像
plt.show()

image-20220914214839705

4.8 与预训练的DenseNet结果对比

这里直接利用迁移学习,将在ImageNet模型上预训练的DenseNet模型用来训练,结果如下:

image-20220914215005295

acc:

image-20220914215014394

loss:

image-20220914215029243

可以看到,有没有预训练对结果的影响很大,而且预训练的权重都是别人经过大量实验得出的,所以后面训练模型的时候尽量用迁移学习方法去做,自己从头训练的话太慢了,效果还不行。


版权声明:本文为qq_43753724原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。