经典神经网络(7)DenseNet及其在Fashion-MNIST数据集上的应用

经典神经网络(7)DenseNet及其在Fashion-MNIST数据集上的应用

1 DenseNet的简述

  1. DenseNet不是通过更深或者更宽的结构,而是通过特征重用来提升网络的学习能力。

  2. ResNet 的思想是:创建从“靠近输入的层” 到 “靠近输出的层” 的直连。而DenseNet 做得更为彻底:将所有层以前馈的形式相连,这种网络因此称作DenseNet

  3. DenseNet 具有以下的优点:

    • 缓解梯度消失的问题。因为每层都可以直接从损失函数中获取梯度、从原始输入中获取信息,从而易于训练。
    • 密集连接还具有正则化的效应,缓解了小训练集任务的过拟合。
    • 鼓励特征重用。网络将不同层学到的 feature map 进行组合。
    • 大幅度减少参数数量。因为每层的卷积核尺寸都比较小,输出通道数较少 (由增长率决定)。
  4. DenseNet 具有比传统卷积网络更少的参数,因为它不需要重新学习多余的feature map

    • 传统的前馈神经网络可以视作在层与层之间传递状态的算法,每一层接收前一层的状态,然后将新的状态传递给下一层。

      这会改变状态,但是也传递了需要保留的信息。

    • ResNet 通过恒等映射来直接传递需要保留的信息,因此层之间只需要传递状态的变化

    • DenseNet 会将所有层的状态 全部保存到集体知识中,同时每一层增加很少数量的feture map 到网络的集体知识中

  5. DenseNet 的层很窄(即:feature map 的通道数很小),如:每一层的输出只有 12 个通道。

  6. 在跨层连接上,不同于ResNet中将输⼊与输出相加,稠密连接网络(DenseNet)在通道维上连结输⼊与输出。DenseNet的主要构建模块是稠密块和过渡层。在构建DenseNet时,我们需要通过添加过渡层来控制网络的维数,从⽽再次减少通道的数量。

  7. 虽然 DenseNet 的计算效率较高、参数相对较少,但是DenseNet 对内存不友好。可以考虑通过共享内存,解决这个问题。

  8. 论文下载地址: https://arxiv.org/pdf/1608.06993.pdf

1.1 稠密块(dense block)

在这里插入图片描述

在这里插入图片描述

ResNet和DenseNet的关键区别在于,DenseNet输出是连接(下图中的[ , ] 表示),而不是如ResNet的简单相加。

在这里插入图片描述

DenseNet这个名字由变量之间的“稠密连接”⽽得来,最后⼀层与之前的所有层紧密相连。

在这里插入图片描述

注意:当 feature map 的尺寸改变时,无法沿着通道方向进行拼接。此时将网络划分为多个DenseNet 块,每块内部的 feature map尺寸相同,块之间的feature map 尺寸不同。

1.1.1 增长率

  1. DenseNet 块中,每层的 H(即BN-ReLU-Conv) 输出的feature map 通道数都相同,都是k个。 k是一个重要的超参数,称作网络的增长率。

    第 l 层的输入【特征图】的通道数为: k 0 + k ( l − 1 ) 。其中 k 0 为输入层的通道数。 第l层的输入【特征图】 的通道数为:k_0 + k(l-1) 。其中k_0为输入层的通道数。 l层的输入【特征图】的通道数为:k0+k(l1)。其中k0为输入层的通道数。

  2. DenseNet 不同于现有网络的一个重要地方是:DenseNet 的网络很窄,即输出的 feature map 通道数较小,如:k = 12 。

    • 一个很小的增长率就能够获得不错的效果。一种解释是:DenseNet 块的每层都可以访问块内的所有早前层输出的feature map,这些feature map 可以视作DenseNet 块的全局状态。每层输出的feature map 都将被添加到块的这个全局状态中,该全局状态可以理解为网络块的【集体知识】,由块内所有层共享。增长率决定了新增特征占全局状态的比例。

    • 因此feature map 无需逐层复制(因为它是全局共享),这也是DenseNet 与传统网络结构不同的地方。这有助于整个网络的特征重用,并产生更紧凑的模型。

1.1.2 非线性变换

  • H可以是包含了 Batch Normalization(BN) 、ReLU 单元、池化或者卷积等操作的复合函数。

  • 论文中的结构为:先执行BN ,再执行ReLU,最后接一个3 x 3 的卷积,即:BN-ReLU-Conv(3x3)

  • pytorch实现如下

import torch.nn as nn
import torch


'''
DenseNet使⽤了ResNet改良版的“批量规范化、激活和卷积”架构

    卷积块:BN-ReLU-Conv
'''
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)
         )

1.1.3 bottleneck

在这里插入图片描述

1.1.4 pytorch实现稠密块

import torch.nn as nn
import torch


'''
DenseNet使⽤了ResNet改良版的“批量规范化、激活和卷积”架构

    卷积块:BN-ReLU-Conv
'''
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)
            # 连接通道维度上每个块的输⼊和输出
            X = torch.cat((X, Y), dim=1)
        return X

if __name__ == '__main__':
    '''
    1、稠密块 dense block
    我们定义⼀个有2个输出通道数为10的DenseBlock。
    使⽤通道数为3的输⼊时,我们会得到通道数为3 + 2 × 10 = 23的输出。
    卷积块的通道数控制了输出通道数相对于输⼊通道数的增⻓,因此也被称为增⻓率(growth rate)。
    '''
    blk = DenseBlock(2, 3, 10)
    # X经过第一个卷积块后变为(4, 10, 8, 8),然后和原始X(4, 3, 8, 8)进行在维度1进行拼接,X变成(4, 13, 8, 8)
    # 然后输入到第二个卷积块,第二个卷积块将channels由(10+3)变为10,因此输出Y(4, 10, 8, 8)
    # 然后X和Y在维度1进行拼接,得到最终输出(4, 23, 8, 8)
    X = torch.randn(4, 3, 8, 8)
    Y = blk(X)
    print(Y.shape)  # (4, 23, 8, 8)

1.2 过渡层(transition layer)

1.2.1 过渡层的介绍

  • 一个DenseNet 网络具有多个DenseNet块,DenseNet 块之间由过渡层连接。DenseNet 块之间的层称为过渡层,其主要作用是连接不同的DenseNet块。

  • 过渡层可以包含卷积或池化操作,从而改变前一个DenseNet 块的输出feature map 的大小(包括尺寸大小、通道数量)。

    • 论文中的过渡层由一个BN层、一个1x1 卷积层、一个2x2 平均池化层组成。其中 1x1 卷积层用于减少DenseNet 块的输出通道数,提高模型的紧凑性。
    • 如果不减少DenseNet 块的输出通道数,则经过了 个DenseNet 块之后,网络的feature map 的通道数会变得很大(通道数计算如下图公式)

在这里插入图片描述

  • 如果Dense 块输出feature map的通道数为 m,则可以使得过渡层输出feature map 的通道数为 theta ✖ m ,其中0< theta <=1 为压缩因子。
    • 当theta = 1时,经过过渡层的feature map 通道数不变。
    • 当theta < 1时,经过过渡层的feature map 通道数减小。此时的DenseNet 称做 DenseNet-C
    • 结合了DenseNet-CDenseNet-B 的改进的网络称作 DenseNet-BC

1.2.2 过渡层的实现

'''
由于每个稠密块都会带来通道数的增加,使⽤过多则会过于复杂化模型。
⽽过渡层可以⽤来控制模型复杂度。它通过1 × 1卷积层来减⼩通道数,并使⽤步幅为2的平均汇聚层减半⾼和宽,从⽽进⼀步降低模型复杂度。
'''
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), # 1×1卷积层来减⼩通道数
            nn.AvgPool2d(kernel_size=2, stride=2)                   # 步幅为2的平均汇聚层减半⾼和宽
    )




if __name__ == '__main__':
    '''
    1、稠密块 dense block
    我们定义⼀个有2个输出通道数为10的DenseBlock。
    使⽤通道数为3的输⼊时,我们会得到通道数为3 + 2 × 10 = 23的输出。
    卷积块的通道数控制了输出通道数相对于输⼊通道数的增⻓,因此也被称为增⻓率(growth rate)。
    '''
    blk = DenseBlock(2, 3, 10)
    # X经过第一个卷积块后变为(4, 10, 8, 8),然后和原始X(4, 3, 8, 8)进行在维度1进行拼接,X变成(4, 13, 8, 8)
    # 然后输入到第二个卷积块,第二个卷积块将channels由(10+3)变为10,因此输出Y(4, 10, 8, 8)
    # 然后X和Y在维度1进行拼接,得到最终输出(4, 23, 8, 8)
    X = torch.randn(4, 3, 8, 8)
    Y = blk(X)
    print(Y.shape)  # (4, 23, 8, 8)


    '''
    2、过渡层 transition layer
    '''
    blk = transition_block(23, 10)
    print(blk(Y).shape)  # torch.Size([4, 10, 4, 4])

1.3 DenseNet网络性能

1.3.1 网络结构

网络结构:ImageNet 训练的DenseNet 网络结构,其中增长率k = 32 。

  • 表中的 conv 代表的是 BN-ReLU-Conv 的组合。如 1x1 conv 表示:先执行BN,再执行ReLU,最后执行1x1 的卷积。
  • DenseNet-xx 表示DenseNet 块有xx 层。如:DenseNet-169 表示 DenseNet 块有L=169 层 。
  • 所有的 DenseNet 使用的是 DenseNet-BC 结构,输入图片尺寸为224x224,初始卷积尺寸为7x7、输出通道 2k、步长为2 ,压缩因子 theta=0.5。
  • 在所有DenseNet 块的最后接一个全局平均池化层,该池化层的结果作为softmax 输出层的输入。

在这里插入图片描述

1.3.2 在ImageNet 验证集的错误率

下图是DenseNetResNetImageNet 验证集的错误率的比较(single-crop)。左图为参数数量,右图为计算量。

在这里插入图片描述

从实验可见:DenseNet 的参数数量和计算量相对ResNet 明显减少。

  • 具有 20M 个参数的DenseNet-201 与具有 40M 个参数的ResNet-101 验证误差接近。
  • ResNet-101 验证误差接近的DenseNet-201 的计算量接近于ResNet-50,几乎是ResNet-101 的一半。

1.3.3 一个简单版本DenseNet的实现

我们实现一个简单的版本的DenseNet,使用DenseNet,而非DenseNet-BC,以应用在Fashion-MNIST数据集上。

稠密块和过度层

import torch.nn as nn
import torch


'''
DenseNet使⽤了ResNet改良版的“批量规范化、激活和卷积”架构

    卷积块:BN-ReLU-Conv
'''
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)
            # 连接通道维度上每个块的输⼊和输出
            X = torch.cat((X, Y), dim=1)
        return X

'''
由于每个稠密块都会带来通道数的增加,使⽤过多则会过于复杂化模型。
⽽过渡层可以⽤来控制模型复杂度。它通过1 × 1卷积层来减⼩通道数,并使⽤步幅为2的平均汇聚层减半⾼和宽,从⽽进⼀步降低模型复杂度。
'''
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), # 1×1卷积层来减⼩通道数
            nn.AvgPool2d(kernel_size=2, stride=2)                   # 步幅为2的平均汇聚层减半⾼和宽
    )


if __name__ == '__main__':
    '''
    1、稠密块 dense block
    我们定义⼀个有2个输出通道数为10的DenseBlock。
    使⽤通道数为3的输⼊时,我们会得到通道数为3 + 2 × 10 = 23的输出。
    卷积块的通道数控制了输出通道数相对于输⼊通道数的增⻓,因此也被称为增⻓率(growth rate)。
    '''
    blk = DenseBlock(2, 3, 10)
    # X经过第一个卷积块后变为(4, 10, 8, 8),然后和原始X(4, 3, 8, 8)进行在维度1进行拼接,X变成(4, 13, 8, 8)
    # 然后输入到第二个卷积块,第二个卷积块将channels由(10+3)变为10,因此输出Y(4, 10, 8, 8)
    # 然后X和Y在维度1进行拼接,得到最终输出(4, 23, 8, 8)
    X = torch.randn(4, 3, 8, 8)
    Y = blk(X)
    print(Y.shape)  # (4, 23, 8, 8)


    '''
    2、过渡层 transition layer
    '''
    blk = transition_block(23, 10)
    print(blk(Y).shape)  # torch.Size([4, 10, 4, 4])

DenseNet

import torch.nn as nn
import torch
from _08_dense_block import DenseBlock,transition_block

class DenseNet(nn.Module):


    def __init__(self):
        super(DenseNet, self).__init__()
        '''
            1、DenseNet⾸先使⽤同ResNet⼀样的单卷积层和最⼤汇聚层。
        '''
        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)
        )
        '''
            2、接下来,类似于ResNet使⽤的4个残差块,DenseNet使⽤的是4个稠密块。
        与ResNet类似,我们可以设置每个稠密块使⽤多少个卷积层。这⾥我们设成4,从⽽之前的ResNet-18保持⼀致。
        稠密块⾥的卷积层通道数(即增⻓率)设为32,所以每个稠密块将增加128个通道。
        
        
            3、在每个模块之间,ResNet通过步幅为2的残差块减⼩⾼和宽,DenseNet则使⽤过渡层来减半⾼和宽,并减半通道数。
        '''
        # num_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))
            # 上⼀个稠密块的输出通道数
            num_channels += num_convs * growth_rate

            # 在稠密块之间添加⼀个转换层,使通道数量减半
            if i != len(num_convs_in_dense_blocks) - 1:
                blks.append(transition_block(num_channels, num_channels // 2))
                num_channels = num_channels // 2
        '''
        4、与ResNet类似,最后接上全局汇聚层和全连接层来输出结果。
        '''
        self.model = nn.Sequential(
            b1,
            *blks,
            nn.BatchNorm2d(num_channels),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Flatten(),
            nn.Linear(num_channels, 10)
        )




    def forward(self, X):
        return self.model(X)


if __name__ == '__main__':
    net = DenseNet()
    X = torch.rand(size=(1, 1, 224, 224), dtype=torch.float32)
    for layer in net.model:
        X = layer(X)
        print(layer.__class__.__name__, 'output shape:', X.shape)
Sequential output shape: torch.Size([1, 64, 56, 56])

DenseBlock output shape: torch.Size([1, 192, 56, 56])
Sequential output shape: torch.Size([1, 96, 28, 28])
DenseBlock output shape: torch.Size([1, 224, 28, 28])
Sequential output shape: torch.Size([1, 112, 14, 14])
DenseBlock output shape: torch.Size([1, 240, 14, 14])
Sequential output shape: torch.Size([1, 120, 7, 7])
DenseBlock output shape: torch.Size([1, 248, 7, 7])

BatchNorm2d output shape: torch.Size([1, 248, 7, 7])
ReLU output shape: torch.Size([1, 248, 7, 7])
AdaptiveAvgPool2d output shape: torch.Size([1, 248, 1, 1])
Flatten output shape: torch.Size([1, 248])
Linear output shape: torch.Size([1, 10])

1.4 DenseNet的内存或显存消耗过多问题

虽然 DenseNet 的计算效率较高、参数相对较少,但是DenseNet 对内存不友好。考虑到GPU 显存大小的限制,因此无法训练较深的 DenseNet

1.4.1 内存的计算

假设DenseNet块包含L层,则:
对于第 l 层,有 x l = H l ( [ x 0 , x 1 , . . . , x l − 1 ] ) 对于第l层,有x_l = H_l([x_0,x_1,...,x_{l-1}]) 对于第l层,有xl=Hl([x0,x1,...,xl1])
假设每层的输出feature map 尺寸均为W×H、通道数为k, 由BN-ReLU-Conv(3x3) 组成,则:

  • 拼接Concat操作 :需要生成临时feature map 作为第l层的输入,内存消耗为 W×H×k×l 。
  • BN 操作:需要生成临时feature map 作为ReLU 的输入,内存消耗为 W×H×k×l 。
  • ReLU 操作:可以执行原地修改,因此不需要额外的feature map 存放ReLU 的输出。
  • Conv 操作:需要生成输出feature map 作为第l层的输出,它是必须的开销。

因此除了第1,2,…,L层的输出feature map 需要内存开销之外,第l层还需要2W×H×k×l 的内存开销来存放中间生成的临时feature map
整个 D e n s e N e t 块需要 W × H × k × ( L + 1 ) L 的内存开销来存放中间生成的临时特征图。 即 D e n s e N e t 块的内存消耗为 O ( L 2 ) ,是网络深度的平方关系。 整个 DenseNet 块 需要W×H×k×(L+1)L 的内存开销来存放中间生成的临时特征图。\\ 即DenseNet 块 的内存消耗为O(L^2),是网络深度的平方关系。 整个DenseNet块需要W×H×k×(L+1)L的内存开销来存放中间生成的临时特征图。DenseNet块的内存消耗为O(L2),是网络深度的平方关系。

1.4.2 拼接的必要性及内存消耗的原因

  • 拼接Concat操作是必须的,因为当卷积的输入存放在连续的内存区域时,卷积操作的计算效率较高。而DenseNet Block 中,第l层的输入feature map 由前面各层的输出feature map 沿通道方向拼接而成。而这些输出feature map 并不在连续的内存区域。

  • DenseNet Block 的这种内存消耗并不是DenseNet Block 的结构引起的,而是由深度学习库引起的。因为Tensorflow/PyTorch 等库在实现神经网络时,会存放中间生成的临时节点(如BN 的输出节点),这是为了在反向传播阶段可以直接获取临时节点的值。

  • 这是在时间代价和空间代价之间的折中:通过开辟更多的空间来存储临时值,从而在反向传播阶段节省计算。

1.4.3 网络的参数也会消耗内存

除了临时feature map 的内存消耗之外,网络的参数也会消耗内存。设 H由BN-ReLU-Conv(3x3) 组成,则第l层的网络参数数量为: 9×l×k^2(不考虑 BN )。
整个 D e n s e N e t 块的参数数量为 9 k 2 ( L + 1 ) L 2 , 即 O ( L 2 ) 整个 DenseNet块的参数数量为\frac{9k^2(L+1)L}{2},即O(L^2) 整个DenseNet块的参数数量为29k2(L+1)L,O(L2)

  • 由于DenseNet 参数数量与网络的深度呈平方关系,因此DenseNet 网络的参数更多、网络容量更大。这也是DenseNet 优于其它网络的一个重要因素。
  • 通常情况下都有WH > (9×k/2) ,其中W,H为网络feature map 的宽、高,k为网络的增长率。所以网络参数消耗的内存要远小于临时feature map 消耗的内存。

1.5 DenseNet内存优化_共享内存

其思想是利用时间代价和空间代价之间的折中,但是侧重于牺牲时间代价来换取空间代价。

其背后支撑的因素是:Concat操作和BN 操作的计算代价很低,但是空间代价很高。因此这种做法在DenseNet 中非常有效。

1.5.1 传统做法

传统的DenseNet Block 的第 l 层。首先将 feature map 拷贝到连续的内存块,拷贝时完成拼接的操作。然后依次执行BNReLUConv 操作。

该层的临时feature map 需要消耗内存 2W×H×k×l,该层的输出feature map 需要消耗内存W×H×k 。

  • 另外某些实现(如LuaTorch)还需要为反向传播过程的梯度分配内存,如左图下半部分所示。如:计算 BN 层输出的梯度时,需要用到第 l 层输出层的梯度和BN 层的输出。存储这些梯度需要额外的 O(lk)的内存。
  • 另外一些实现(如PyTorch,MxNet)会对梯度使用共享的内存区域来存放这些梯度,因此只需要O(k)的内存。

在这里插入图片描述

1.5.2 共享内存做法

右图为内存优化的DenseNet Block 的第 l 层。采用两组预分配的共享内存区Shared memory Storage location 来存concate 操作和BN 操作输出的临时feature map

对于第一组预分配的共享内存区:

第一组预分配的共享内存区:concat 操作共享区。第1,2,…,L 层的 concat 操作的输出都写入到该共享区,第(l+1) 层的写入会覆盖第 (l)层的结果。

  • 对于整个Dense Block,这个共享区只需要分配 W×H×k×L(最大的feature map )的内存,即内存消耗为O(kL) (对比传统DenseNet的O(kL^2) )。

  • 后续的BN 操作直接从这个共享区读取数据。

  • 由于第 (l+1)层的写入会覆盖第(l) 层的结果,因此这里存放的数据是临时的、易丢失的。因此在反向传播阶段还需要重新计算第 (l)层的Concat 操作的结果。

    因为concat 操作的计算效率非常高,因此这种额外的计算代价很低。

对于第二组预分配的共享内存区

第二组预分配的共享内存区:BN 操作共享区。第1,2,…,L 层的 concat 操作的输出都写入到该共享区,第(l+1) 层的写入会覆盖第 (l)层的结果。

  • 对于整个Dense Block,这个共享区也只需要分配W×H×k×L (最大的feature map )的内存,即内存消耗为O(kL) (对比传统DenseNet的O(kL^2) )。

  • 后续的卷积操作直接从这个共享区读取数据。

  • concat 操作共享区同样的原因,在反向传播阶段还需要重新计算第(l)层的BN 操作的结果。

    BN 的计算效率也很高,只需要额外付出大约 5% 的计算代价。

由于BN 操作和concat 操作在神经网络中大量使用,因此这种预分配共享内存区的方法可以广泛应用。它们可以在增加少量的计算时间的情况下节省大量的内存消耗。

2 DenseNet在Fashion-MNIST数据集上的应用示例

2.1 创建DenseNet网络模型

如1.3.3所示。

2.2 读取Fashion-MNIST数据集

batch_size = 256

# 为了使Fashion-MNIST上的训练短⼩精悍,将输⼊的⾼和宽从224降到96,简化计算
train_iter,test_iter = get_mnist_data(batch_size,resize=96)

2.3 在GPU上进行模型训练

from _08_DenseNet import DenseNet

# 初始化模型
net = DenseNet()

lr, num_epochs = 0.1, 10
train_ch(net, train_iter, test_iter, num_epochs, lr, try_gpu())

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44665283/article/details/131558669