欢迎关注『youcans动手学模型』系列
本专栏内容和资源同步到 GitHub/youcans
【youcans动手学模型】DenseNet 模型-CIFAR10图像分类
- 1. DenseNet 神经网络模型
- 1.1 模型简介
- 1.2 论文介绍
- 1.3 改进方法与后续工作
- 1.4 分析与讨论
- 2. 在 PyTorch 中定义 DenseNet 模型类
- 2.1 DenseBlock 模块
- 2.2 Transition 过渡层
- 2.3 DenseNet 模型类
- 2.4 DenseNet 模型类之二
- 2.5 从 torchvision.model 加载预定义模型
- 3. 基于 DenseNet 模型的 CIFAR10 图像分类
- 3.1 PyTorch 建立神经网络模型的基本步骤
- 3.2 加载 CIFAR10 数据集
- 3.3 建立 DenseNet 网络模型
- 3.4 DenseNet模型训练
- 3.5 DenseNet 模型的保存与加载
- 3.6 模型检验
- 3.7 模型推理
- 4. DenseNet 模型对 CIFAR10 进行图像分类的完整例程
- 参考文献:
本文用 PyTorch 实现 DenseNet 网络模型,使用 CIFAR10 数据集训练模型,进行图像分类。
1. DenseNet 神经网络模型
Gao Huang 等在 2016 年发表的论文“Densely Connected Convolutional Networks”,提出了 DenseNet 网络模型,本文作者来自 Cornell 大学和 Facebook,获得了 CVPR 2017 最佳论文。
【下载地址】
Gao Huang, Zhuang Liu, et al. 2016, Densely Connected Convolutional Networks
Geoff Pleiss, Danlu Chen, Gao Huang, et al. 2017, Memory-Efficient Implementation of DenseNets
【GitHub地址】https://github.com/liuzhuang13/DenseNet
【PyTorch实现】https://github.com/gpleiss/efficient_densenet_pytorch
1.1 模型简介
DenseNet 是一种网络架构,提出了“密集块(DenseBlock)”的模型结构。
在每个密集块(DenseBlock)内,每一层都以前馈方式直接连接到其他每一层。对于每一层,所有先前层的特征图都被视为单独的输入,而其自身的特征图被作为输入传递给所有后续层。
作者发现通过类似 Dropout 的方法随机扔掉一些层,能够提高 ResNet 的泛化能力,于是设计了DenseNet。
DenseNet 受到 ResNet 的影响和启发, 将 ResNet 中 residual connection 的思想发挥到了极致 。DenseNet 做了两个重要的设计:
-
一是网络的每一层都直接与其前面层相连,实现特征的重复使用;
-
二是网络的每一层都很窄,达到降低冗余性的目的。
DenseNet 与 ResNet 的区别在于,ResNet 中通过相加(element-wise adding)来实现跨层连接,而 DenseNet 中通过拼接(concatenate)来实现跨层连接。
R e s N e t : x l = H l ( x l − 1 ) + x l − 1 D e n s e N e t : x l = H l ( [ x 0 , x 1 , . . . x l − 1 ] ) \begin{matrix} ResNet: &x_l = H_l (x_{l-1}) + x_{l-1} \\ DenseNet: &x_l = H_l ([x_0, x_1,...x_{l-1}]) \end{matrix} ResNet:DenseNet:xl=Hl(xl−1)+xl−1xl=Hl([x0,x1,...xl−1])
DenseNet 很容易训练,但有很多数据重复使用,因此显存占用很大。更新的版本通过用时间换空间的方法,将 DenseLayer 中部分数据使用完就释放,需要时再重新计算,通过增加一些计算量来节约大量的内存空间。
这种连接模式在 CIFAR10/100(有或没有数据增强)和 SVHN上产生了最先进的精度。在 ImageNet 数据集上,DenseNet 实现了 与ResNet 类似的精度,但使用的参数数量不到一半,FLOP数量大约是一半。
1.2 论文介绍
【论文摘要】
如果卷积网络在靠近输入的层和靠近输出的层之间包含较短的连接,那么它们可以更深入、更准确、更高效地进行训练。
本文引入了密集卷积网络(DenseNet),它以前馈的方式将每一层连接到另一层。具有 L 层的传统卷积网络具有 L 个连接,每层及其后续层之间有一个连接——而 DenseNet 网络具有 L ∗ ( L + 1 ) / 2 L*(L+1)/2 L∗(L+1)/2 个直接连接。对于每个层,所有先前层的特征图都用作输入,并且其自身的特征图也被用作所有后续层的输入。DenseNet 的优势在于: 它们缓解了消失梯度问题,加强了特征传播,鼓励了特征重用,并大大减少了参数的数量。
我们在四个基准识别任务(CIFAR-10、CIFAR-100、SVHN 和 ImageNet)上评估了 DenseNet 模型。DenseNet 在大多数方面都比目前最先进的技术有了显著的改进,同时需要更少的内存和计算才能实现高性能。
【论文背景】
卷积神经网络的结构正在变得越来越更深。早期的 LeNet5 由 5层组成,VGG 由 19层组成,ResNet 超过了100层。随着卷积神经网络结构变得更深,由于输入或梯度信息要通过许多层,因此出现了梯度消失的问题。
ResNets 和 Highway Networks 通过恒等映射(Identity mappings)使用跨层的跳跃连接来解决梯度消失问题,随机深度方法(Stochastic depth)通过随机丢弃层来缩短 ResNet。FractalNets 将几个平行层的序列与不同数量的卷积块组合,同时保持多条旁路路径。这些方法以不同方案构造跨层的连接路径。
DenseNet 为了确保网络层之间最多的信息流,将每个网络层与其前面的所有层相连。每个层从前面的所有层获得额外的输入,并将自己的特征映射传递到后续的所有层。与 ResNets 不同的是,对于前层的特征不是通过加权求和来实现,而是通过将它们拼接起来进行组合。
【模型结构】
DenseNet 模型的核心是前文介绍的“密集块(DenseBlock)”。
DenseBlock 中第 l l l 层的输出为:
x l = H l ( [ x 0 , x 1 , . . . x l − 1 ] ) x_l = H_l ([x_0, x_1,...x_{l-1}]) xl=Hl([x0,x1,...xl−1])
将第 l 层之前的各层的输出进行拼接,作为第 l l l 层的输入。
H l H_l Hl 由 3个连续的操作 BN-ReLU-Conv 组成:BN(batch normalization),ReLU 和 3*3 卷积。
如果每一层都输出 k 个特征图,则第 l 层有 k 0 + ( l − 1 ) ∗ k k_0+(l-1)*k k0+(l−1)∗k 个特征图输入。k 是卷积层的深度,即提取的特征数,由于每一层都接受了前面所有层的特征,所以 k 可以很小,例如取 k=12/24/40。与之前的 VGG、ResNet 等模型相比,DenseBlock 的网络层很窄。
DenseBlock 使用过多会导致模型复杂化,而过渡层可以用来控制模型复杂度。过渡层位于 DenseBlock 之后,通过 1*1 卷积层来减小通道数,并使用步幅为 2 的平均汇聚层使高宽减半,降低模型的尺寸和复杂度 。
完整的 DenseNet 模型结构如下。
- DenseNet 模型包括 3个 DenseBlock,每个 DenseBlock 的层数不等。3 个 DenseBlock 中的特征图大小分别为 32*32、16*16 和 8*8。
- 输入图像经过一个卷积层(7*7 conv,stride=2)和池化层(3*3 maxpool, stride=2),然后进入第一个 DenseBlock。
- 每两个 DenseBlock 之间,使用 1*1 的卷积层和 2*2 的平均池化层作为过渡层,特征图尺寸减半。
- 第三个 DenseBlock 结束后,经过平均池化层,最后进入 softmax 分类器。
在 ImageNet 上使用的网络模型的配置如下表所示。
【模型性能】
【论文结论】
本文提出了一种新的卷积网络架构,称之为密集卷积网络(DenseNet),它引入了具有相同特征图大小的任意两个层之间的直接连接。
- DenseNet 可以简单地扩展到数百层,在训练上没有困难。
- DenseNet 随着参数数量的增加,往往会在精度上持续提高,而没有任何性能下降或过拟合。
- DenseNet 在几个竞争激烈的数据集上取得了最先进的结果。
- DenseNet 只要更少的参数和更少的计算,就可以实现最先进的性能。
由于我们在研究中采用了针对残差网络优化的超参数设置,我们认为可以通过更细致地调整超参数和学习速率来进一步提高 DenseNet 模型的准确性。
DenseNet 通过简单的连接规则,自然融合了恒等映射(Identity mappings)、深度监督(Deep supervision)和深度多样化(Diversified depth)的特性,为复杂任务提供强大的模型表达能力。 通过在整个网络中重用特征,可以学习更紧凑的模型,学习更准确的模型。
由于其紧凑的内部表示和减少的特征冗余,DenseNet 可能是基于卷积特征的各种计算机视觉任务的良好特征提取器,我们将在未来的工作中研究基于 DenseNet 的特征迁移。
恒等映射是指 ResNet 中跳跃连接和加和项,使得前向和反向的信号能直接在模块之间传播,可以缓解深度神经网络训练中的梯度消失和爆炸问题。
深度监督是指在不同的隐藏层添加监督信息,以防止过拟合,增强模型的泛化性能。
深度多样化是指模型在设计上具有不同深度的网络结构,通过改变模型的深度影响模型的表达能力,从而改善模型在不同任务上的性能。
1.3 改进方法与后续工作
【改进方法】
2017年,作者在论文 “Memory-Efficient Implementation of DenseNets” 对 DenseNet 模型进行了改进,解决内存占用太大的问题。
由于特征重用,DenseNet 模型在计算上非常高效,但需要大量的 GPU 内存。如果管理不当,批量归一化和连续卷积操作可能导致特征图数量随网络深度的平方而增大。
本文介绍了在训练过程中减少 DenseNet 内存消耗的策略。通过使用共享内存分配,将存储特征图的内存从二次的平方关系降低到线性关系。该策略可以训练非常深的DenseNet:具有 14M 参数的网络可以在单个 GPU 上进行训练;以前无法训练 264 层的 DenseNet(73M参数),现在可以在带有 8个 NVIDIA Tesla M40 GPU 的单个工作站上进行训练。在 ImageNet ILSVRC 分类数据集上,这种大型 DenseNet 的 Top-1 错误率达到了 20.26%。
【后续工作】
- Multi-Scale Dense Convolutional Networks for Efficient Prediction (https://github.com/gaohuang/MSDNet)
- DSOD: Learning Deeply Supervised Object Detectors from Scratch (https://github.com/szq0214/DSOD)
- denseNet: An Efficient DenseNet using Learned Group Convolutions (https://github.com/ShichenLiu/CondenseNet)
- Fully Convolutional DenseNets for Semantic Segmentation (https://github.com/SimJeg/FC-DenseNet)
- Pelee: A Real-Time Object Detection System on Mobile Devices (https://github.com/Robert-JunWang/Pelee)
1.4 分析与讨论
DenseNet 模型的优点是:
(1)参数量小,在 ImageNet 数据集上达到同样准确率,DenseNet 所需的参数量不到 ResNet 的一半。
(2)计算量小,在达到同样精度的情况下,DenseNet 所需的计算量只有 ResNet 的一半。
(3)防止过拟合, DenseNet 具有非常好的抗过拟合性能,尤其适合于训练数据相对匮乏的应用。
(4)泛化性能好,如果不进行数据增强,ResNet 在 CIFAR100 数据集的性能下降很多,而 DenseNet 的性能下降较小。
对于 DenseNet 防止过拟合的原因,直观的解释是,每个卷积层特征提取都相当于对输入数据的非线性变换,复杂度随着网络深度而增大。传统卷积神经网络的分类器仅依赖于最后一层卷积层的特征,复杂度最高。而 DenseNet 利用了不同层次的特征,具有更强大的特征表达能力。因此更容易得到比较平滑的决策边界,即使在训练数据不足时仍然表现良好。
很多读者也会对 DenseNet 有一些疑问,作者给予了答复:
(1)密集连接会不会导致参数量和计算量大?
「密集连接」的网络结构给人的第一感觉就是极大的增加了网络的参数量和计算量。但实际上恰恰相反,DenseNet 比其他网络效率更高,参数量和计算机更少,关键在于网络每层计算量的减少以及特征的重复利用。
虽然 DenseBlock 连接密集,但每一层只需学习很少的特征,特征图数量 k 很小, 可以很好的降低参数量 。ResNet 网络的参数量与卷积层深度(特征图数量)的平方 C*C 成正比,虽然DenseNet 网络的参数量与 l*k*k成正比。但由于 k << C,因此 DenseNet 的参数量和计算量小得多。
(2)密集连接会不会带来大量冗余?
论文中的热力图(heatmap)直观上刻画了各个连接的强度,图中显示网络中非常浅层的特征也会被使用。当然,采用适当的剪枝方法,可以删除一些连接而不会影响性能。
(3)DenseNet 特别耗费显存?
在模型训练时,可以通过预先分配一块缓存,供网络中所有的拼接层(Concatenation Layer)共享使用,可以使内存消耗便从平方级别降到了线性级别。
在模型推理时,由于 DenseNet 每一层产生的特征图很少,所以占用内存并不会多于其他模型。
2. 在 PyTorch 中定义 DenseNet 模型类
DenseNet 模型是一种网络框架,针对不同的任务可以进行不同的超参数配置。
2.1 DenseBlock 模块
DenseBlock 模块是 DenseNet 的核心,通过逐层拼接实现跨层连接。
DenseBlock 模块例程如下,在例程基础上还可以进一步将一些超参数设置为输入参数。
# 定义 DenseBlock 模块
class DenseBlock(nn.Module):
def __init__(self, in_channels):
super(DenseBlock, self).__init__()
self.relu = nn.ReLU(inplace=True)
self.bn = nn.BatchNorm2d(num_features=in_channels)
self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=32, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=3, stride=1, padding=1)
self.conv3 = nn.Conv2d(in_channels=64, out_channels=32, kernel_size=3, stride=1, padding=1)
self.conv4 = nn.Conv2d(in_channels=96, out_channels=32, kernel_size=3, stride=1, padding=1)
self.conv5 = nn.Conv2d(in_channels=128, out_channels=32, kernel_size=3, stride=1, padding=1)
def forward(self, x):
bn = self.bn(x)
conv1 = self.relu(self.conv1(bn))
conv2 = self.relu(self.conv2(conv1))
# Concatenate in channel dimension
c2_dense = self.relu(torch.cat([conv1, conv2], 1))
conv3 = self.relu(self.conv3(c2_dense))
c3_dense = self.relu(torch.cat([conv1, conv2, conv3], 1))
conv4 = self.relu(self.conv4(c3_dense))
c4_dense = self.relu(torch.cat([conv1, conv2, conv3, conv4], 1))
conv5 = self.relu(self.conv5(c4_dense))
c5_dense = self.relu(torch.cat([conv1, conv2, conv3, conv4, conv5], 1))
return c5_dense
2.2 Transition 过渡层
多个 DenseBlock 之间使用 Transition 过渡层,通常使用1*1 卷积层来减小通道数,使用步幅为 2 的平均汇聚层进行下采样 ,也可以加入 ReLu 和 BN。
Transition 过渡层的例程如下。
# 定义 Transition 层
class Transition(nn.Module):
def __init__(self, in_channels, out_channels):
super(Transition, self).__init__()
self.relu = nn.ReLU(inplace=True)
self.bn = nn.BatchNorm2d(num_features=out_channels)
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
self.meanpool = nn.AvgPool2d(kernel_size=2, stride=2, padding=0)
def forward(self, x):
bn = self.bn(self.relu(self.conv(x)))
out = self.meanpool(bn)
return out
2.3 DenseNet 模型类
PyTorch 通过 torch.nn 模块提供了高阶的 API,可以从头开始构建网络。
一个面向 CIFAR10 数据集图像分类问题的 DenseNet 定义如下。该模型与 DenseNet 论文原文的结构基本一致,特征提取器包括 3 个 DenseBlocks,每个 DenseBlocks 之后使用 Transition 过渡层,DenseBlocks 和 Transition 调用上节的程序实现。
# 定义 DenseNet 模型类
class DenseNet(nn.Module):
def __init__(self, num_classes=10):
super(DenseNet, self).__init__()
self.lowconv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7, padding=3, bias=False)
self.relu = nn.ReLU()
# Make Dense Blocks
self.DenseBlock1 = self._make_dense_block(DenseBlock, 64)
self.DenseBlock2 = self._make_dense_block(DenseBlock, 128)
self.DenseBlock3 = self._make_dense_block(DenseBlock, 128)
# Make transition Layers
self.Transition1 = self._make_transition_layer(Transition, in_channels=160, out_channels=128)
self.Transition2 = self._make_transition_layer(Transition, in_channels=160, out_channels=128)
self.Transition3 = self._make_transition_layer(Transition, in_channels=160, out_channels=64)
# Classifier
self.bn = nn.BatchNorm2d(num_features=64)
self.pre_classifier = nn.Linear(64 * 4 * 4, 512)
self.classifier = nn.Linear(512, num_classes)
def _make_dense_block(self, block, in_channels):
layers = []
layers.append(block(in_channels))
return nn.Sequential(*layers)
def _make_transition_layer(self, layer, in_channels, out_channels):
modules = []
modules.append(layer(in_channels, out_channels))
return nn.Sequential(*modules)
def forward(self, x):
# 卷积神经网络用于特征提取
out = self.relu(self.lowconv(x)) # 32x32x3 -> 32x32x64
out = self.DenseBlock1(out) # 32x32x64 -> 32x32x160
out = self.Transition1(out) # 32x32x160 -> 16x16x128
out = self.DenseBlock2(out) # 16x16x128 -> 16x16x160
out = self.Transition2(out) # 16x16x160 -> 8x8x128
out = self.DenseBlock3(out) # 8x8x128 -> 8x8x160
out = self.Transition3(out) # 8x8x160 -> 4x4x64
out = self.bn(out) # 4x4x64 -> 4x4x64
out = out.view(-1, 64*4*4) # 4x4x64 -> 1024
# 全连接神经网络用于分类
out = self.pre_classifier(out) # 1024 -> 512
out = self.classifier(out) # 512 -> 10
return out
2.4 DenseNet 模型类之二
DenseNet 模型是一种网络框架,针对不同的任务可以进行不同的超参数配置。本节介绍另一种 DenseNet 模型类,来自李沐等【动手学深度学习(PyTorch版)】。
from torch import nn
def conv_block(input_channels, num_channels):
return nn.Sequential(
nn.BatchNorm2d(input_channels), nn.ReLU(),
nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1))
class DenseBlock(nn.Module):
def __init__(self, num_convs, input_channels, num_channels):
super(DenseBlock, self).__init__()
layer = []
for i in range(num_convs):
layer.append(conv_block(
num_channels * i + input_channels, num_channels))
self.net = nn.Sequential(*layer)
def forward(self, X):
for blk in self.net:
Y = blk(X)
# Concatenate the input and output of each block on the channel
# dimension
X = torch.cat((X, Y), dim=1)
return X
def transition_block(input_channels, num_channels):
return nn.Sequential(
nn.BatchNorm2d(input_channels), nn.ReLU(),
nn.Conv2d(input_channels, num_channels, kernel_size=1),
nn.AvgPool2d(kernel_size=2, stride=2))
DenseNet 首先使用单卷积层和最大汇聚层处理,然后使用多个 DenseBlock 和 TransitionBlock,最后使用全局汇聚层和全连接层实现分类器。
b1 = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
# `num_channels`: the current number of channels
num_channels, growth_rate = 64, 32
num_convs_in_dense_blocks = [4, 4, 4, 4]
blks = []
for i, num_convs in enumerate(num_convs_in_dense_blocks):
blks.append(DenseBlock(num_convs, num_channels, growth_rate))
# This is the number of output channels in the previous dense block
num_channels += num_convs * growth_rate
# A transition layer that haves the number of channels is added between
# the dense blocks
if i != len(num_convs_in_dense_blocks) - 1:
blks.append(transition_block(num_channels, num_channels // 2))
num_channels = num_channels // 2
net = nn.Sequential(
b1, *blks,
nn.BatchNorm2d(num_channels), nn.ReLU(),
nn.AdaptiveMaxPool2d((1, 1)),
nn.Flatten(),
nn.Linear(num_channels, 10))
2.5 从 torchvision.model 加载预定义模型
我们可以从头开始搭建各种网络模型,有利于学习和分析。但是自己搭建模型的效率低,容易出错。
Torchvision 自带了很多经典的网络模型,可以直接加载这些预定义模型。我们可以只使用预定义的模型类来创建实例化模型对象(不加载预训练的模型参数)用于模型训练,也可以在实例化模型对象的同时加载预训练的模型参数,还可以基于预训练模型进行模型微调或迁移学习。
本节从 torchvision.model 加载 DenseNet 预定义模型,而不从 torchvision 加载预训练模型参数。实例化 DenseNet 模型类,使用 CIFAR10 数据集训练自己的 DenseNet 模型。
torchvision.model 提供了 densenet121
、densenet161
、densenet169
、densenet201
模型类和预训练模型可以直接使用,但这些预训练模型是在 ImageNet 数据集进行训练,图片尺寸和分类类别都与 CIFAR10 数据集不同,不能直接用于训练 CIFAR10 数据集。
torchvision.model 也提供了 torchvision.models.densenet.DenseNet 基类,我们可以直接调用 DenseNet 基类,来实例化用于 CIFAR10 数据集训练的 densenet 模型。DenseNet 基类的参数定义如下,更多原始代码可以参考:SOURCE CODE FOR TORCHVISION.MODELS.DENSENET。
class DenseNet(nn.Module):
r"""Densenet-BC model class, based on
`"Densely Connected Convolutional Networks" <https://arxiv.org/pdf/1608.06993.pdf>`_.
Args:
growth_rate (int) - how many filters to add each layer (`k` in paper)
block_config (list of 4 ints) - how many layers in each pooling block
num_init_features (int) - the number of filters to learn in the first convolution layer
bn_size (int) - multiplicative factor for number of bottle neck layers
(i.e. bn_size * k features in the bottleneck layer)
drop_rate (float) - dropout rate after each dense layer
num_classes (int) - number of classification classes
memory_efficient (bool) - If True, uses checkpointing. Much more memory efficient,
but slower. Default: *False*. See `"paper" <https://arxiv.org/pdf/1707.06990.pdf>`_.
"""
def __init__(
self,
growth_rate: int = 32,
block_config: Tuple[int, int, int, int] = (6, 12, 24, 16),
num_init_features: int = 64,
bn_size: int = 4,
drop_rate: float = 0,
num_classes: int = 1000,
memory_efficient: bool = False,
) -> None:
调用 DenseNet 基类,实例化用于 CIFAR10 数据集训练的 densenet 模型的程序如下。
from torchvision import transforms, models
# (3) 从 torchvision.model 加载预定义模型 DenseNet (不加载模型权值)
model = models.DenseNet(num_init_features=32, num_classes=10) # 实例化 DenseNet 模型类
model.to(device) # 将网络分配到指定的device中
其中,num_init_features=32 用于设置输入图片的大小为 (32,32),num_classes=10 用于设置分类的类别数量为 10。
3. 基于 DenseNet 模型的 CIFAR10 图像分类
3.1 PyTorch 建立神经网络模型的基本步骤
使用 PyTorch 建立、训练和使用神经网络模型的基本步骤如下。
- 准备数据集(Prepare dataset):加载数据集,对数据进行预处理。
- 建立模型(Design the model):实例化模型类,定义损失函数和优化器,确定模型结构和训练方法。
- 模型训练(Model trainning):使用训练数据集对模型进行训练,确定模型参数。
- 模型推理(Model inferring):使用训练好的模型进行推理,对输入数据预测输出结果。
- 模型保存与加载(Model saving/loading):保存训练好的模型,以便以后使用或部署。
以下按此步骤讲解 DenseNet 模型的例程。
3.2 加载 CIFAR10 数据集
通用数据集的样本结构均衡、信息高效,而且组织规范、易于处理。使用通用的数据集训练神经网络,不仅可以提高工作效率,而且便于评估模型性能。
PyTorch 提供了一些常用的图像数据集,预加载在 torchvision.datasets
类中。torchvision
模块实现神经网络所需的核心类和方法, torchvision.datasets
包含流行的数据集、模型架构和常用的图像转换方法。
CIFAR 数据集是一个经典的图像分类小型数据集,有 CIFAR10 和 CIFAR100 两个版本。CIFAR10 有 10 个类别,CIFAR100 有 100 个类别。CIFAR10 每张图像大小为 32*32,包括飞机、小汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车 10 个类别。CIFAR10 共有 60000张图像,其中训练集 50000张,测试集 10000张。每个类别有 6000张图片,数据集平衡。
加载和使用 CIFAR 数据集的方法为:
torchvision.datasets.CIFAR10()
torchvision.datasets.CIFAR100()
CIFAR 数据集可以从官网下载:http://www.cs.toronto.edu/~kriz/cifar.html 后使用,也可以使用 datasets 类自动加载(如果本地路径没有该文件则自动下载)。
下载数据集时,使用预定义的 transform 方法进行数据预处理,包括调整图像尺寸、标准化处理,将数据格式转换为张量。标准化处理所使用 CIFAR10 数据集的均值和方差为 (0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)。transform_train在训练过程中,增加随机性,提高泛化能力。
大型训练数据集不能一次性加载全部样本来训练,可以使用 Dataloader 类自动加载数据。Dataloader 是一个迭代器,基本功能是传入一个 Dataset 对象,根据参数 batch_size 生成一个 batch 的数据。
使用 DataLoader 类加载 CIFAR-10 数据集的例程如下。
# (1) 将[0,1]的PILImage 转换为[-1,1]的Tensor
transform_train = transforms.Compose([
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomRotation(10), # 随机旋转
transforms.RandomAffine(0, shear=10, scale=(0.8, 1.2)),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
transforms.Resize((32, 32)), # 图像大小调整为 (w,h)=(32,32)
transforms.ToTensor(), # 将图像转换为张量 Tensor
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))
])
# 测试集不需要进行数据增强
transform = transforms.Compose([
transforms.Resize((32, 32)), # 图像大小调整为 (w,h)=(32,32)
transforms.ToTensor(), # 将图像转换为张量 Tensor
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))
])
# (2) 加载 CIFAR10 数据集
batchsize = 128
# 加载 CIFAR10 数据集, 如果 root 路径加载失败, 则自动在线下载
# 加载 CIFAR10 训练数据集, 50000张训练图片
train_set = torchvision.datasets.CIFAR10(root='../dataset', train=True,
download=True, transform=transform_train)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batchsize)
# 加载 CIFAR10 验证数据集, 10000张验证图片
test_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=1000)
# 创建生成器,用 next 获取一个批次的数据
valid_data_iter = iter(test_loader) # _SingleProcessDataLoaderIter 对象
valid_images, valid_labels = next(valid_data_iter) # images: [batch,3,224,224], labels: [batch]
valid_size = valid_labels.size(0) # 验证数据集大小,batch
print(valid_images.shape, valid_labels.shape)
# 定义类别名称,CIFAR10 数据集的 10个类别
classes = ('plane', 'car', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck')
3.3 建立 DenseNet 网络模型
建立一个 DenseNet 网络模型进行训练,包括三个步骤:
- 实例化 DenseNet 模型对象;
- 设置训练的损失函数;
- 设置训练的优化器。
为了使用 GPU 设备进行模型训练和模型推理,使用 model.to(device) 将网络分配到指定的设备中。
torch.nn.functional 模块提供了各种内置损失函数,本例使用交叉熵损失函数 CrossEntropyLoss。
torch.optim 模块提供了各种优化方法。本例使用 SGD 优化器,有研究报道指出 SGD 优化器对于 DenseNet 模型收敛性能更好。注意要将 model 的参数 model.parameters() 传给优化器对象,以便扫描需要优化的参数。
# (3) 构造 DenseNet 网络模型
model = DenseNet(num_classes=10) # 实例化 DenseNet 网络模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device) # 将网络分配到指定的device中
# print(model)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 定义损失函数 CrossEntropy
optimizer = torch.optim.SGD(model.parameters(), momentum=0.8, lr=0.01) # 定义优化器 SGD
3.4 DenseNet模型训练
PyTorch 模型训练的基本步骤是:
- 前馈计算模型的输出值;
- 计算损失函数值;
- 计算权重 weight 和偏差 bias 的梯度;
- 根据梯度值调整模型参数;
- 将梯度重置为 0(用于下一循环)。
在模型训练过程中,可以使用验证集数据评价训练过程中的模型精度,以便控制训练过程。模型验证就是用验证数据进行模型推理,前向计算得到模型输出,但不反向计算模型误差,因此需要设置 torch.no_grad()。
使用 PyTorch 进行模型训练的例程如下。
# (4) 训练 DenseNet 网络模型
epoch_list = [] # 记录训练轮次
loss_list = [] # 记录训练集的损失值
accu_list = [] # 记录验证集的准确率
num_epochs = 100 # 训练轮次
for epoch in range(num_epochs): # 训练轮次 epoch
running_loss = 0.0 # 每个轮次的累加损失值清零
for step, data in enumerate(train_loader, start=0): # 迭代器加载数据
optimizer.zero_grad() # 损失梯度清零
inputs, labels = data # inputs: [batch,3,224,224] labels: [batch]
outputs = model(inputs.to(device)) # 正向传播
loss = criterion(outputs, labels.to(device)) # 计算损失函数
loss.backward() # 反向传播
optimizer.step() # 参数更新
# 累加训练损失值
running_loss += loss.item()
# if step%100==99: # 每 100 个 step 打印一次训练信息
# print("\t epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))
# 计算每个轮次的验证集准确率
with torch.no_grad(): # 验证过程, 不计算损失函数梯度
outputs_valid = model(valid_images.to(device)) # 模型对验证集进行推理, [batch, 10]
pred_labels = torch.max(outputs_valid, dim=1)[1] # 预测类别, [batch]
accuracy = torch.eq(pred_labels, valid_labels.to(device)).sum().item() / valid_size * 100 # 计算准确率
print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))
# 记录训练过程的统计数据
epoch_list.append(epoch) # 记录迭代次数
loss_list.append(running_loss) # 记录训练集的损失函数
accu_list.append(accuracy) # 记录验证集的准确率
程序运行结果如下:
Epoch 0: train loss=571.8047, accuracy=59.80%
Epoch 1: train loss=431.3222, accuracy=67.00%
Epoch 2: train loss=368.1180, accuracy=73.30%
Epoch 3: train loss=329.6648, accuracy=74.70%
…
Epoch 98: train loss=66.0069, accuracy=89.30%
Epoch 99: train loss=68.7796, accuracy=89.10%
经过 20 轮左右的训练,使用验证集中的 1000 张图片进行验证,模型准确率达到 85%。继续训练可以进一步降低训练损失函数值,经过 100轮左右的训练验证集的准确率保持在 88~90%。
3.5 DenseNet 模型的保存与加载
模型训练好以后,将模型保存起来,以便下次使用。PyTorch 中模型保存主要有两种方式,一是保存模型权值,二是保存整个模型。本例使用 model.state_dict() 方法以字典形式返回模型权值,torch.save() 方法将权值字典序列化到磁盘,将模型保存为 .pth 文件。
由于本例程中模型存储在 CUDA 设备上,在保存模型时要将模型移动到 CPU。
# (5) 保存 DenseNet 网络模型
save_path = "../models/DenseNet_Cifar2"
model_cpu = model.cpu() # 将模型移动到 CPU
model_path = save_path + ".pth" # 模型文件路径
torch.save(model.state_dict(), model_path) # 保存模型权值
使用训练好的模型,首先要实例化模型类,然后调用 load_state_dict() 方法加载模型的权值参数。
# 以下模型加载和模型推理,可以是另一个独立的程序
# (6) 加载 DenseNet 网络模型进行推理
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测并指定设备
# 加载 DenseNet 预训练模型
model = DenseNet(num_classes=10) # 实例化 DenseNet 网络模型
model.to(device) # 将网络分配到指定的device中
model_path = "../models/DenseNet_Cifar1.pth"
model.load_state_dict(torch.load(model_path))
model.eval() # 模型推理模式
需要特别注意的是:
(1)PyTorch 中的 .pth 文件只保存了模型的权值参数,而没有模型的结构信息,因此必须先实例化模型对象,再加载模型参数。
(2)模型对象必须与模型参数严格对应,才能正常使用。注意即使都是 DenseNet 模型,模型类的具体定义也可能有细微的区别。如果从一个来源获取模型类的定义,从另一个来源获取模型参数文件,就很容易造成模型结构与参数不能匹配。
(3)无论从 PyTorch 模型仓库加载的模型和参数,或从其它来源获取的预训练模型,或自己训练得到的模型,模型加载的方法都是相同的,也都要注意模型结构与参数的匹配问题。
3.6 模型检验
使用加载的 DenseNet 模型,输入新的图片进行模型推理,可以由模型输出结果确定输入图片所属的类别。
使用测试集数据进行模型推理,根据模型预测结果与图片标签进行比较,可以检验模型的准确率。模型验证集与模型检验集不能交叉使用,但为了简化例程在本程序中未做区分。
# (7) 模型检验
correct = 0
total = 0
for data in test_loader: # 迭代器加载测试数据集
imgs, labels = data # torch.Size([batch,3,224,224]) torch.Size([batch])
# print(imgs.shape, labels.shape)
outputs = model(imgs.to(device)) # 正向传播, 模型推理, [batch, 10]
labels_pred = torch.max(outputs, dim=1)[1] # 模型预测的类别 [batch]
# _, labels_pred = torch.max(outputs.data, 1)
total += labels.size(0)
correct += torch.eq(labels_pred, labels.to(device)).sum().item()
accuracy = 100. * correct / total
print("Test samples: {}".format(total))
print("Test accuracy={:.2f}%".format(accuracy))
使用测试集进行模型推理,测试模型准确率为 88.30%。
Test samples: 10000
Test accuracy=88.30%
3.7 模型推理
使用加载的 DenseNet 模型,输入新的图片进行模型推理,可以由模型输出结果确定输入图片所属的类别。
从测试集中提取几张图片,或者读取图像文件,进行模型推理,获得图片的分类类别。在提取图片或读取文件时,要注意对图片格式和图片大小进行适当的转换。
# (8) 提取测试集图片进行模型推理
batch = 8 # 批次大小
data_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
download=False, transform=None)
plt.figure(figsize=(9, 6))
for i in range(batch):
imgPIL = data_set[i][0] # 提取 PIL 图片
label = data_set[i][1] # 提取 图片标签
# 预处理/模型推理/后处理
imgTrans = transform(imgPIL) # 预处理变换, torch.Size([3,32,32])
imgBatch = torch.unsqueeze(imgTrans, 0) # 转为批处理,torch.Size([batch=1,3,32,32])
outputs = model(imgBatch.to(device)) # 模型推理, 返回 [batch=1, 10]
indexes = torch.max(outputs, dim=1)[1] # 注意 [batch=1], device = 'device
index = indexes[0].item() # 预测类别,整数
# 绘制第 i 张图片
imgNP = np.array(imgPIL) # PIL -> Numpy
out_text = "label:{}/model:{}".format(classes[label], classes[index])
plt.subplot(2, 4, i+1)
plt.imshow(imgNP)
plt.title(out_text)
plt.axis('off')
plt.tight_layout()
plt.show()
结果如下。
# (9) 读取图像文件进行模型推理
from PIL import Image
filePath = "../images/img_plane_01.jpg" # 数据文件的地址和文件名
imgPIL = Image.open(filePath) # PIL 读取图像文件, <class 'PIL.Image.Image'>
# 预处理/模型推理/后处理
imgTrans = transform["test"](imgPIL) # 预处理变换, torch.Size([3, 224, 224])
imgBatch = torch.unsqueeze(imgTrans, 0) # 转为批处理,torch.Size([batch=1, 3, 224, 224])
outputs = model(imgBatch.to(device)) # 模型推理, 返回 [batch=1, 10]
indexes = torch.max(outputs, dim=1)[1] # 注意 [batch=1], device = 'device
percentages = nn.functional.softmax(outputs, dim=1)[0] * 100
index = indexes[0].item() # 预测类别,整数
percent = percentages[index].item() # 预测类别的概率,浮点数
# 绘制第 i 张图片
imgNP = np.array(imgPIL) # PIL -> Numpy
out_text = "Prediction:{}, {}, {:.2f}%".format(index, classes[index], percent)
print(out_text)
plt.imshow(imgNP)
plt.title(out_text)
plt.axis('off')
plt.tight_layout()
plt.show()
结果如下。
4. DenseNet 模型对 CIFAR10 进行图像分类的完整例程
本文的完整例程如下。
# Begin_DenseNet_CIFAR_2.py
# DenseNet model for beginner with PyTorch
# 经典模型: 使用 DenseNet 模型 进行 CIFAR10 图像分类,使用 Torchvision 预定义模型
# 使用 torchvision.models.densenet.DenseNet 类
# Copyright: youcans@qq.com
# Crated: Huang Shan, 2023/05/20
# _*_coding:utf-8_*_
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
from torchvision import transforms, models
from matplotlib import pyplot as plt
import numpy as np
# 优化结果写入数据文件
import pandas as pd
def WriteDataFile(epoch_list, loss_list, accu_list, filepath):
# print("def WriteDataFile()")
optRecord = {
"epoch": epoch_list,
"train_loss": loss_list,
"accuracy": accu_list}
dfRecord = pd.DataFrame(optRecord)
dfRecord.to_csv(filepath, index=False, encoding="utf_8_sig")
print("写入数据文件: %s 完成。" % filepath)
return
if __name__ == '__main__':
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
# (1) 将[0,1]的PILImage 转换为[-1,1]的Tensor
transform_train = transforms.Compose([
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.RandomRotation(10), # 随机旋转
transforms.RandomAffine(0, shear=10, scale=(0.8, 1.2)),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
transforms.Resize((32, 32)), # 图像大小调整为 (w,h)=(32,32)
transforms.ToTensor(), # 将图像转换为张量 Tensor
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))
])
# 测试集不需要进行数据增强
transform = transforms.Compose([
transforms.Resize((32, 32)), # 图像大小调整为 (w,h)=(32,32)
transforms.ToTensor(), # 将图像转换为张量 Tensor
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261))
])
# (2) 加载 CIFAR10 数据集
batchsize = 128
# 加载 CIFAR10 数据集, 如果 root 路径加载失败, 则自动在线下载
# 加载 CIFAR10 训练数据集, 50000张训练图片
train_set = torchvision.datasets.CIFAR10(root='../dataset', train=True,
download=True, transform=transform_train)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batchsize)
# 加载 CIFAR10 验证数据集, 10000张验证图片
test_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=1000)
# 创建生成器,用 next 获取一个批次的数据
valid_data_iter = iter(test_loader) # _SingleProcessDataLoaderIter 对象
valid_images, valid_labels = next(valid_data_iter) # images: [batch,3,224,224], labels: [batch]
valid_size = valid_labels.size(0) # 验证数据集大小,batch
print(valid_images.shape, valid_labels.shape)
# 定义类别名称,CIFAR10 数据集的 10个类别
classes = ('plane', 'car', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck')
# (3) 从 torchvision.model 加载预定义模型 DenseNet (不加载模型权值)
model = models.DenseNet(num_init_features=32, num_classes=10) # 实例化 DenseNet 模型类
model.to(device) # 将网络分配到指定的 device中
# print(model)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 定义损失函数 CrossEntropy
optimizer = torch.optim.SGD(model.parameters(), momentum=0.8, lr=0.01) # 定义优化器 SGD
# (4) 训练 DenseNet 网络模型
epoch_list = [] # 记录训练轮次
loss_list = [] # 记录训练集的损失值
accu_list = [] # 记录验证集的准确率
num_epochs = 10 # 训练轮次
for epoch in range(num_epochs): # 训练轮次 epoch
running_loss = 0.0 # 每个轮次的累加损失值清零
for step, data in enumerate(train_loader, start=0): # 迭代器加载数据
optimizer.zero_grad() # 损失梯度清零
inputs, labels = data # inputs: [batch,3,224,224] labels: [batch]
outputs = model(inputs.to(device)) # 正向传播
loss = criterion(outputs, labels.to(device)) # 计算损失函数
loss.backward() # 反向传播
optimizer.step() # 参数更新
# 累加训练损失值
running_loss += loss.item()
# print("\t epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))
if step%100==99: # 每 100 个 step 打印一次训练信息
print("\t epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))
# 计算每个轮次的验证集准确率
with torch.no_grad(): # 验证过程, 不计算损失函数梯度
outputs_valid = model(valid_images.to(device)) # 模型对验证集进行推理, [batch, 10]
pred_labels = torch.max(outputs_valid, dim=1)[1] # 预测类别, [batch]
accuracy = torch.eq(pred_labels, valid_labels.to(device)).sum().item() / valid_size * 100 # 计算准确率
print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))
# 记录训练过程的统计数据
epoch_list.append(epoch) # 记录迭代次数
loss_list.append(running_loss) # 记录训练集的损失函数
accu_list.append(accuracy) # 记录验证集的准确率
# 训练结果可视化
plt.figure(figsize=(11, 5))
plt.suptitle("DenseNet Model in CIFAR10")
plt.subplot(121), plt.title("Train loss")
plt.plot(epoch_list, loss_list)
plt.xlabel('epoch'), plt.ylabel('loss')
plt.subplot(122), plt.title("Valid accuracy")
plt.plot(epoch_list, accu_list)
plt.xlabel('epoch'), plt.ylabel('accuracy')
plt.show()
# (5) 保存 DenseNet 网络模型
save_path = "../models/DenseNet_Cifar2"
model_cpu = model.cpu() # 将模型移动到 CPU
model_path = save_path + ".pth" # 模型文件路径
torch.save(model.state_dict(), model_path) # 保存模型权值
# 优化结果写入数据文件
result_path = save_path + ".csv" # 优化结果文件路径
WriteDataFile(epoch_list, loss_list, accu_list, result_path)
# # 以下模型加载和模型推理,可以是另一个独立的程序
# # (6) 加载 DenseNet 网络模型进行推理
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测并指定设备
# # 加载 DenseNet 预训练模型
# # model = DenseNet(num_classes=10) # 实例化 DenseNet 网络模型
# model = models.DenseNet(num_init_features=32, num_classes=10) # 实例化 DenseNet 模型类
# model.to(device) # 将网络分配到指定的device中
# model_path = "../models/DenseNet_Cifar2.pth"
# model.load_state_dict(torch.load(model_path))
# model.eval() # 模型推理模式
#
# # (7) 模型检测
# correct = 0
# total = 0
# for data in test_loader: # 迭代器加载测试数据集
# imgs, labels = data # torch.Size([batch,3,32,32) torch.Size([batch])
# # print(imgs.shape, labels.shape)
# outputs = model(imgs.to(device)) # 正向传播, 模型推理, [batch, 10]
# labels_pred = torch.max(outputs, dim=1)[1] # 模型预测的类别 [batch]
# # _, labels_pred = torch.max(outputs.data, 1)
# total += labels.size(0)
# correct += torch.eq(labels_pred, labels.to(device)).sum().item()
# accuracy = 100. * correct / total
# print("Test samples: {}".format(total))
# print("Test accuracy={:.2f}%".format(accuracy))
#
# # (8) 提取测试集图片进行模型推理
# batch = 8 # 批次大小
# data_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
# download=False, transform=None)
# plt.figure(figsize=(9, 6))
# for i in range(batch):
# imgPIL = data_set[i][0] # 提取 PIL 图片
# label = data_set[i][1] # 提取 图片标签
# # 预处理/模型推理/后处理
# imgTrans = transform(imgPIL) # 预处理变换, torch.Size([3,32,32])
# imgBatch = torch.unsqueeze(imgTrans, 0) # 转为批处理,torch.Size([batch=1,3,32,32])
# outputs = model(imgBatch.to(device)) # 模型推理, 返回 [batch=1, 10]
# indexes = torch.max(outputs, dim=1)[1] # 注意 [batch=1], device = 'device
# index = indexes[0].item() # 预测类别,整数
# # 绘制第 i 张图片
# imgNP = np.array(imgPIL) # PIL -> Numpy
# out_text = "label:{}/model:{}".format(classes[label], classes[index])
# plt.subplot(2, 4, i+1)
# plt.imshow(imgNP)
# plt.title(out_text)
# plt.axis('off')
# plt.tight_layout()
# plt.show()
#
# # (9) 读取图像文件进行模型推理
# from PIL import Image
# filePath = "../images/img_plane_01.jpg" # 数据文件的地址和文件名
# imgPIL = Image.open(filePath) # PIL 读取图像文件, <class 'PIL.Image.Image'>
#
# # 预处理/模型推理/后处理
# imgTrans = transform(imgPIL) # 预处理变换, torch.Size([3, 224, 224])
# imgBatch = torch.unsqueeze(imgTrans, 0) # 转为批处理,torch.Size([batch=1, 3, 224, 224])
# outputs = model(imgBatch.to(device)) # 模型推理, 返回 [batch=1, 10]
# indexes = torch.max(outputs, dim=1)[1] # 注意 [batch=1], device = 'device
# percentages = nn.functional.softmax(outputs, dim=1)[0] * 100
# index = indexes[0].item() # 预测类别,整数
# percent = percentages[index].item() # 预测类别的概率,浮点数
#
# # 绘制第 i 张图片
# imgNP = np.array(imgPIL) # PIL -> Numpy
# out_text = "Prediction:{}, {}, {:.2f}%".format(index, classes[index], percent)
# print(out_text)
# plt.imshow(imgNP)
# plt.title(out_text)
# plt.axis('off')
# plt.tight_layout()
# plt.show()
【参考文献】:
- Gao Huang, Zhuang Liu, et al. Densely Connected Convolutional Networks, 2016
- Geoff Pleiss, Danlu Chen, Gao Huang, et al. Memory-Efficient Implementation of DenseNets, 2017
- 阿斯顿.张, 李沐等, 动手学深度学习(PyTorch版), 人民邮电出版社, 2023
参考文献:
- Gao Huang, Zhuang Liu, et al. Densely Connected Convolutional Networks, 2016
- Geoff Pleiss, Danlu Chen, Gao Huang, et al. Memory-Efficient Implementation of DenseNets, 2017
- 阿斯顿.张, 李沐等, 动手学深度学习(PyTorch版), 人民邮电出版社, 2023
【本节完】
版权声明:
欢迎关注『youcans动手学模型』系列
转发请注明原文链接:
【youcans动手学模型】DenseNet 模型-CIFAR10图像分类
Copyright 2023 youcans, XUPT
Crated:2023-06-10