目录
- 一、理论基础
- 1.前言
- 2.设计理念
- 3.网络结构
- 4.与其他算法效果对比
- 二、 前期准备
- 1. 设置GPU
- 2. 导入数据
- 3. 划分数据集
- 三、搭建网络模型
- 1. DenseLayer模块
- 2. DenseBlock模块
- 3. Transition模块
- 4. 构建DenseNet
- 5. 构建densenet121
- 四、 训练模型
- 1. 编写训练函数
- 2. 编写测试函数
- 3. 正式训练
- 五、 结果可视化
- 1. Loss与Accuracy图
- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊|接辅导、项目定制
一、理论基础
1.前言
在计算机视觉领域,卷积神经网络(CNN)已经成为最主流的方法,比如GoogLenet,VGG-16,Incepetion等模型。CNN史上的一个里程碑事件是ResNet模型的出现,ResNet可以训练出更深的CNN模型,从而实现更高的准确度。ResNet模型的核心是通过建立前面层与后面层之间的“短路连接”(shortcuts,skip connection),进而训练出更深的CNN网络。
今天我们要介绍的是DenseNet模型,它的基本思路与ResNet一致,但是它建立的是前面所有层与后面层的密集连接(dense connection),它的名称也是由此而来。DenseNet的另一大特色是通过特征在channel上的连接来实现特征重用(feature reuse)。这些特点让DenseNet在参数和计算成本更少的情形下实现比ResNet更优的性能,DenseNet也因此斩获CVPR 2017的最佳论文奖。
2.设计理念
相比ResNet,DenseNet提出了一个更激进的密集连接机制:即互相连接所有的层,具体来说就是每个层都会接受其前面所有层作为其额外的输入。
图1为ResNet网络的残差连接机制,作为对比,图2为DenseNet的密集连接机制。可以看到,ResNet是每个层与前面的某层(一般是2~4层)短路连接在一起,连接方式是通过元素相加。而在DenseNet中,每个层都会与前面所有层在channel维度上连接(concat)在一起(即元素叠加),并作为下一层的输入。
对于一个
L
L
L层的网络,DenseNet共包含
L
(
L
+
1
)
2
\frac{L(L+1)}{2}
2L(L+1)个连接,相比ResNet,这是一种密集连接。而且DenseNet是直接concat来自不同层的特征图,这可以实现特征重用,提升效率,这一特点是DenseNet与ResNet最主要的区别。
●输入和输出的公式是
X
l
=
H
l
(
X
l
−
1
)
X_l = H_l(X_{l-1})
Xl=Hl(Xl−1),其中
H
l
H_l
Hl是一个组合函数,通常包括 BN、ReLU、Pooling、Conv操作,
X
l
−
1
X_{l-1}
Xl−1是第
l
l
l层输入的特征图,
X
l
X_{l}
Xl是第
l
l
l层输出的特征图。
● ResNet是跨层相加,输入和输出的公式是
X
l
=
H
l
(
X
l
−
1
)
+
X
l
−
1
X_l = H_l(X_{l-1})+X_{l-1}
Xl=Hl(Xl−1)+Xl−1
3.网络结构
具体介绍网络的具体实现细节如图4所示。
CNN网络一般要经过Pooling或者stride>1的Conv来降低特征图的大小,而DenseNet的密集连接方式需要特征图大小保持一致。为了解决这个问题,DenseNet网络中使用DenseBlock+Transition的结构,其中DenseBlock是包含很多层的模块,每个层的特征图大小相同,层与层之间采用密集连接方式。而Transition层是连接两个相邻的DenseBlock,并且通过Pooling使特征图大小降低。图5给出了DenseNet的网路结构,它共包含4个DenseBlock,各个DenseBlock之间通过Transition层连接在一起。
在DenseBlock中,各个层的特征图大小一致,可以在channel维度上连接。DenseBlock中的非线性组合函数
H
(
⋅
)
H(\cdot)
H(⋅)的是 BN+ReLU+3x3 Conv 的结构,如图6所示。另外值得注意的一点是,与ResNet不同,所有DenseBlock中各个层卷积之后均输出
k
k
k个特征图,即得到的特征图的channel数为
k
k
k,或者说采用
k
k
k个卷积核。
k
k
k在DenseNet称为growth rate,这是一个超参数。一般情况下使用较小的
k
k
k(比如12),就可以得到较佳的性能。假定输入层的特征图的channel数为
k
0
k_0
k0,那么
l
l
l层输入的channel数为
k
0
+
k
(
1
,
2
,
.
.
.
,
l
−
1
)
k_0+k_{(1,2,...,l-1)}
k0+k(1,2,...,l−1),因此随着层数增加,尽管
k
k
k设定得较小,DenseBlock的输入会非常多,不过这是由于特征重用所造成的,每个层仅有
k
k
k个特征是自己独有的。
由于后面层的输入会非常大,DenseBlock内部可以采用bottleneck层来减少计算量,主要是原有的结构中增加1x1 Conv,如图7所示,即BN+ReLU+1x1 Conv+BN+ReLU+3x3 Conv,称为DenseNet-B结构。其中1x1 Conv得到
4
k
4k
4k个特征图它起到的作用是降低特征数量,从而提升计算效率。
对于Transition层,它主要是连接两个相邻的DenseBlock,并且降低特征图大小。Transition层包括一个1x1的卷积和2x2的AvgPooling,结构为BN+ReLU+1x1Conv+2x2AvgPooling。另外,Transition层可以起到压缩模型的作用。假定Transition层的上接DenseBlock得到的特征图channels数为
m
m
m,Transition层可以产生
⌊
θ
m
⌋
\lfloor\theta m\rfloor
⌊θm⌋个特征(通过卷积层),其中
θ
∈
(
0
,
1
]
\theta \in (0,1]
θ∈(0,1]是压缩系数(compression rate)。当
θ
=
1
\theta=1
θ=1时,特征个数经过Transition层没有变化,即无压缩,而当压缩系数小于1时,这种结构称为DenseNet-C,文中使用
θ
=
0.5
\theta=0.5
θ=0.5。对于使用bottleneck层的DenseBlock结构和压缩系数小于1的Transition组合结构称为DenseNet-BC。
对于ImageNet数据集,图片输入大小为 224 × 224 224\times 224 224×224,网络结构采用包含4个DenseBlock的DenseNet-BC,其首先是一个stride=2的7x7卷积层,然后是一个stride=2的3x3 MaxPooling层,后面才进入DenseBlock。ImageNet数据集所采用的网络配置如表1所示:
4.与其他算法效果对比
二、 前期准备
🏡 我的环境:
● 语言环境:Python3.8
● 编译器:Jupyter Lab
● 深度学习环境:Pytorch
○ torch1.12.1+cu113
○ torchvision0.13.1+cu113
1. 设置GPU
如果设备上支持GPU就使用GPU,否则使用CPU
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision
from torchvision import transforms, datasets
import os,PIL,pathlib,warnings
warnings.filterwarnings("ignore") #忽略警告信息
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
2. 导入数据
import os,PIL,random,pathlib
data_dir = './J3-data/'
data_dir = pathlib.Path(data_dir)
data_paths = list(data_dir.glob('*'))
classeNames = [str(path).split("\\")[1] for path in data_paths]
classeNames
# 关于transforms.Compose的更多介绍可以参考:https://blog.csdn.net/qq_38251616/article/details/124878863
train_transforms = transforms.Compose([
transforms.Resize([224, 224]), # 将输入图片resize成统一尺寸
# transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.ToTensor(), # 将PIL Image或numpy.ndarray转换为tensor,并归一化到[0,1]之间
transforms.Normalize( # 标准化处理-->转换为标准正太分布(高斯分布),使模型更容易收敛
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]) # 其中 mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的。
])
test_transform = transforms.Compose([
transforms.Resize([224, 224]), # 将输入图片resize成统一尺寸
transforms.ToTensor(), # 将PIL Image或numpy.ndarray转换为tensor,并归一化到[0,1]之间
transforms.Normalize( # 标准化处理-->转换为标准正太分布(高斯分布),使模型更容易收敛
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]) # 其中 mean=[0.485,0.456,0.406]与std=[0.229,0.224,0.225] 从数据集中随机抽样计算得到的。
])
total_data = datasets.ImageFolder(data_dir,transform=train_transforms)
total_data
Dataset ImageFolder
Number of datapoints: 13403
Root location: J3-data
StandardTransform
Transform: Compose(
Resize(size=[224, 224], interpolation=bilinear, max_size=None, antialias=None)
ToTensor()
Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
)
3. 划分数据集
train_size = int(0.8 * len(total_data))
test_size = len(total_data) - train_size
train_dataset, test_dataset = torch.utils.data.random_split(total_data, [train_size, test_size])
train_dataset, test_dataset
batch_size = 32
train_dl = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size,
shuffle=True)
test_dl = torch.utils.data.DataLoader(test_dataset,
batch_size=batch_size,
shuffle=True)
for X, y in test_dl:
print("Shape of X [N, C, H, W]: ", X.shape)
print("Shape of y: ", y.shape, y.dtype)
break
三、搭建网络模型
from collections import OrderedDict
import torch
import torch.nn as nn
import torch.nn.functional as F
1. DenseLayer模块
self.add_module() 函数用于向类中添加一个子模块。
在这段代码中,self.add_module(‘name’, module) 被用于将不同的模块添加到 DenseLayer 类中。这些模块可以是任何继承自 nn.Module 的子类,例如批标准化层 (nn.BatchNorm2d)、ReLU 激活函数 (nn.ReLU)、卷积层 (nn.Conv2d) 等。
该函数的参数包括一个字符串 ‘name’,用于给该模块命名,以及一个模块实例 module,用于表示要添加的模块对象。在代码中,每个模块都按顺序添加到 DenseLayer 类中。
通过使用 self.add_module() 函数,这些子模块被存储在 DenseLayer 类的内部,成为该类的属性。这样,在类的其他方法中,可以通过引用这些属性来访问和操作这些模块,例如在 forward() 方法中使用 super().forward(x) 调用父类模块的前向传播方法。
总之,self.add_module() 函数的作用是将子模块添加到类中,并为这些模块提供属性名以便后续引用和操作。
class DenseLayer(nn.Sequential):
def __init__(self, in_channel, growth_rate, bn_size, drop_rate):
super(DenseLayer, self).__init__()
self.add_module('norm1', nn.BatchNorm2d(in_channel))
self.add_module('relu1', nn.ReLU(inplace=True))
self.add_module('conv1', nn.Conv2d(in_channel, bn_size*growth_rate,
kernel_size=1, stride=1, bias=False))
self.add_module('norm2', nn.BatchNorm2d(bn_size*growth_rate))
self.add_module('relu2', nn.ReLU(inplace=True))
self.add_module('conv2', nn.Conv2d(bn_size*growth_rate, growth_rate,
kernel_size=3, stride=1, padding=1, bias=False))
self.drop_rate = drop_rate
def forward(self, x):
new_feature = super(DenseLayer, self).forward(x)
if self.drop_rate>0:
new_feature = F.dropout(new_feature, p=self.drop_rate, training=self.training)
return torch.cat([x, new_feature], 1)
2. DenseBlock模块
''' DenseBlock '''
class DenseBlock(nn.Sequential):
def __init__(self, num_layers, in_channel, bn_size, growth_rate, drop_rate):
super(DenseBlock, self).__init__()
for i in range(num_layers):
layer = DenseLayer(in_channel+i*growth_rate, growth_rate, bn_size, drop_rate)
self.add_module('denselayer%d'%(i+1,), layer)
3. Transition模块
''' Transition layer between two adjacent DenseBlock '''
class Transition(nn.Sequential):
def __init__(self, in_channel, out_channel):
super(Transition, self).__init__()
self.add_module('norm', nn.BatchNorm2d(in_channel))
self.add_module('relu', nn.ReLU(inplace=True))
self.add_module('conv', nn.Conv2d(in_channel, out_channel,
kernel_size=1, stride=1, bias=False))
self.add_module('pool', nn.AvgPool2d(2, stride=2))
4. 构建DenseNet
nn.Sequential是PyTorch中的一个模型容器,它按照给定的顺序依次执行一系列的神经网络模块(layers)。在构建神经网络时,我们可以使用nn.Sequential来简化代码。
OrderedDict是Python中的一种有序字典数据结构,它保留了元素添加的顺序。在神经网络中,我们可以使用OrderedDict来指定模型的层次结构。
class DenseNet(nn.Module):
def __init__(self, growth_rate=32, block_config=(6,12,24,16), init_channel=64,
bn_size=4, compression_rate=0.5, drop_rate=0, num_classes=1000):
'''
:param growth_rate: (int) number of filters used in DenseLayer, `k` in the paper
:param block_config: (list of 4 ints) number of layers in eatch DenseBlock
:param init_channel: (int) number of filters in the first Conv2d
:param bn_size: (int) the factor using in the bottleneck layer
:param compression_rate: (float) the compression rate used in Transition Layer
:param drop_rate: (float) the drop rate after each DenseLayer
:param num_classes: (int) 待分类的类别数
'''
super(DenseNet, self).__init__()
# first Conv2d
self.features = nn.Sequential(OrderedDict([
('conv0', nn.Conv2d(3, init_channel, kernel_size=7, stride=2, padding=3, bias=False)),
('norm0', nn.BatchNorm2d(init_channel)),
('relu0', nn.ReLU(inplace=True)),
('pool0', nn.MaxPool2d(3, stride=2, padding=1))
]))
# DenseBlock
num_features = init_channel
for i, num_layers in enumerate(block_config):
block = DenseBlock(num_layers, num_features, bn_size, growth_rate, drop_rate)
self.features.add_module('denseblock%d'%(i+1), block)
num_features += num_layers*growth_rate
if i != len(block_config)-1:
transition = Transition(num_features, int(num_features*compression_rate))
self.features.add_module('transition%d'%(i+1), transition)
num_features = int(num_features*compression_rate)
# final BN+ReLU
self.features.add_module('norm5', nn.BatchNorm2d(num_features))
self.features.add_module('relu5', nn.ReLU(inplace=True))
# 分类层
self.classifier = nn.Linear(num_features, num_classes)
# 参数初始化
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1)
elif isinstance(m, nn.Linear):
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.features(x)
x = F.avg_pool2d(x, 7, stride=1).view(x.size(0), -1)
x = self.classifier(x)
return x
5. 构建densenet121
device = "cuda" if torch.cuda.is_available() else "cpu"
print("Using {} device".format(device))
densenet121 = DenseNet(init_channel=64,
growth_rate=32,
block_config=(6,12,24,16),
num_classes=len(classeNames))
model = densenet121.to(device)
model
四、 训练模型
1. 编写训练函数
# 训练循环
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset) # 训练集的大小
num_batches = len(dataloader) # 批次数目, (size/batch_size,向上取整)
train_loss, train_acc = 0, 0 # 初始化训练损失和正确率
for X, y in dataloader: # 获取图片及其标签
X, y = X.to(device), y.to(device)
# 计算预测误差
pred = model(X) # 网络输出
loss = loss_fn(pred, y) # 计算网络输出和真实值之间的差距,targets为真实值,计算二者差值即为损失
# 反向传播
optimizer.zero_grad() # grad属性归零
loss.backward() # 反向传播
optimizer.step() # 每一步自动更新
# 记录acc与loss
train_acc += (pred.argmax(1) == y).type(torch.float).sum().item()
train_loss += loss.item()
train_acc /= size
train_loss /= num_batches
return train_acc, train_loss
2. 编写测试函数
测试函数和训练函数大致相同,但是由于不进行梯度下降对网络权重进行更新,所以不需要传入优化器
def test (dataloader, model, loss_fn):
size = len(dataloader.dataset) # 测试集的大小
num_batches = len(dataloader) # 批次数目, (size/batch_size,向上取整)
test_loss, test_acc = 0, 0
# 当不进行训练时,停止梯度更新,节省计算内存消耗
with torch.no_grad():
for imgs, target in dataloader:
imgs, target = imgs.to(device), target.to(device)
# 计算loss
target_pred = model(imgs)
loss = loss_fn(target_pred, target)
test_loss += loss.item()
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
test_acc /= size
test_loss /= num_batches
return test_acc, test_loss
3. 正式训练
import copy
optimizer = torch.optim.Adam(model.parameters(), lr= 1e-4)
loss_fn = nn.CrossEntropyLoss() # 创建损失函数
epochs = 20
train_loss = []
train_acc = []
test_loss = []
test_acc = []
best_acc = 0 # 设置一个最佳准确率,作为最佳模型的判别指标
for epoch in range(epochs):
model.train()
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, optimizer)
model.eval()
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
# 保存最佳模型到 best_model
if epoch_test_acc > best_acc:
best_acc = epoch_test_acc
best_model = copy.deepcopy(model)
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
# 获取当前的学习率
lr = optimizer.state_dict()['param_groups'][0]['lr']
template = ('Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f}, Test_acc:{:.1f}%, Test_loss:{:.3f}, Lr:{:.2E}')
print(template.format(epoch+1, epoch_train_acc*100, epoch_train_loss,
epoch_test_acc*100, epoch_test_loss, lr))
# 保存最佳模型到文件中
PATH = './best_model.pth' # 保存的参数文件名
torch.save(best_model.state_dict(), PATH)
print('Done')
五、 结果可视化
1. Loss与Accuracy图
import matplotlib.pyplot as plt
#隐藏警告
import warnings
warnings.filterwarnings("ignore") #忽略警告信息
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 100 #分辨率
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()