深度学习入门教程三:Resnet网络搭建和训练

深度学习入门教程一深度学习入门教程二中,分别介绍了如何安装conda、pytorch和利用搭建好的环境进行VGG16的模型的搭建和训练,接下来是另外一篇深度学习中必看和必学的网络结构,这就是Resnet

深度学习入门教程二这篇博客中,我简单地讲解了VGG这篇文章,可以发现VGG提出的最深的模型也不过只有19层,似乎有点违背深度学习这个“深”这个词的意思。其实这Resnet这篇文章就指出,为什么VGG这些之前的网络搭不了这么深,这是由于一个叫梯度爆炸和梯度消失现象所导致的,简单说,太长的正向传播和反向传播导致梯度变化过大,要不就是趋向正无穷,要不就变成0,梯度的两极分布会导致模型无法进行训练,而Resnet这篇文章,就提出使用一个残差结果,去解决上面提到的梯度爆炸和梯度消失的问题。

接下来还是老规矩,先Resnet的论文进行简读

1、Resnet论文简读—Deep Residual Learning for Image Recognition

Resnet的论文名叫Deep Residual Learning for Image Recognition,就是图像识别中的深度残差学习,论文名直接表达了论文的核心内容——残差结构的深度学习网络

摘要

更深的神经网络模型有更好的性能,但是更深的神经网络更难训练。论文提出了一个残差学习框架,以便训练比先前使用的网络深得多的网络。论文明确地重新构造层,将其表述为相对于层输入的学习残差函数,而不是学习无参考的函数。论文提供了全面的经验证据,显示这些残差网络更容易优化,并且可以从更大的深度中获得准确性。

接下来是卖一下Resnet强悍的性能表现

Resnet在ImageNet数据集上,论文评估了深度达152层的残差网络,比VGG网络深8倍,但仍然具有较低的复杂性。这些残差网络的集成在ImageNet测试集上实现了3.57%的错误率。这一结果在ILSVRC 2015分类任务中获得了第一名。我们还在具有100层和1000层的CIFAR-10数据集上进行了分析。(VGG16有约160M的参数量,Resnet仅约20M的参数量,Resnet计算量更低)

对于许多视觉识别任务来说,表示的深度至关重要。仅仅由于我们的极深表示,我们在COCO目标检测数据集上获得了28%的相对改进。Resnet在ILSVRC和COCO 2015数据集、在ImageNet检测、ImageNet定位、COCO检测和COCO分割任务中均获得了第一名。

引言

深度卷积神经网络在图像分类方面取得了一系列突破。深度网络自然地以端到端的多层方式整合了低/中/高级特征[和分类器,而特征的“层次”可以通过堆叠的层数(深度)来丰富。近期的研究表明网络深度至关重要,而在具有挑战性的ImageNet数据集上的领先结果都利用了“非常深”[的模型,深度为十六到三十。许多其他非平凡的视觉识别任务也大大受益于非常深的模型。

由于深度的重要性,一个问题浮现:学习更好的网络是否就像堆叠更多层一样容易?回答这个问题的障碍是臭名昭著的梯度消失/爆炸问题,这从一开始就妨碍了收敛。然而,通过标准初始化和中间标准化层,这个问题已经在很大程度上得到解决,使得具有数十层的网络能够开始收敛,适用于随机梯度下降(SGD)和反向传播。当更深的网络能够开始收敛时,出现了一个退化问题:随着网络深度的增加,准确性会达到饱和(这可能是可以预料的),然后迅速下降。令人意外的是,这种退化并不是由过拟合引起的,向适当深的模型添加更多层会导致更高的训练误差,正如所报告的,并且经过我们的实验证实。图1显示了一个典型的例子。

图1.上图表示网络层数在CIFAR10这个数据集上,网络层数的增加或许不能使错误率下降,也就是说,模型训练不仅仅与网络的层数有关,还和网络的结构有关 

网络层数增加,但是准确率下降表明,并非所有系统都同样容易优化。让论文考虑一个较浅的架构及其添加更多层的深层对应物。存在一种构造深层模型的解决方案:添加的层是恒等映射,其他层是从学到的较浅模型复制的。这个构建解决方案的存在表明,深层模型的训练误差不应比较浅模型的训练误差更高。但实验证明,我们目前手头上的求解器无法找到与构造解决方案相当好或更好的解决方案(或无法在可行时间内做到这一点)。

在本文中,通过引入深度残差学习框架来解决退化问题。论文不再希望每个堆叠的几层直接拟合所需的底层映射,而是明确地让这些层拟合一个残差映射。形式上,将所需的底层映射表示为H(x),我们让堆叠的非线性层拟合另一个映射F(x) := H(x)−x。原始映射重新表述为F(x)+x(这里就是残差的表达公式)。我们假设优化残差映射比优化原始的无参考映射更容易。在极端情况下,如果一个恒等映射是最优的,那么通过将残差推向零要比通过一堆非线性层拟合一个恒等映射更容易。

图2,Resnet中提出的残差结构 

F(x) + x的形式可以通过具有“快捷连接”的前馈神经网络来实现(图2)。这里的“跳跃连接”是指跳过一个或多个层的连接。在论文的情况中,快捷连接简单地执行恒等映射,它们的输出被添加到堆叠层的输出中。恒等快捷连接既不添加额外的参数也不增加计算复杂性。整个网络仍然可以通过带有反向传播的随机梯度下降(SGD)进行端到端训练,并且可以在不修改求解器的情况下轻松使用常见库实现(例如Caffe [19])。

Resnet在ImageNet 上进行了全面的实验,展示了退化问题并评估了我们的方法。我们展示了以下结果:

1)我们的极深残差网络易于优化,但“普通”网络(仅是简单的层叠)在深度增加时表现出更高的训练误差;

2)我们的深度残差网络可以轻松获得由于深度大幅增加而带来的准确性提升,其结果明显优于先前的网络。

类似的现象在CIFAR-10数据集上也得到了展示,这表明我们的方法的优化困难和效果不仅仅适用于特定的数据集。论文成功地在这个数据集上训练了具有100多层的模型,并研究了包含1000多层的模型。

在ImageNet分类数据集上,论文通过极深的残差网络获得了出色的结果。论文的152层残差网络是迄今为止在ImageNet上展示的最深的网络,但其复杂性仍然低于VGG网络。Resnet的整体在ImageNet测试集上的top-5错误率为3.57%,并在ILSVRC 2015分类比赛中获得了第一名。这些极深的表示还在其他识别任务中表现出色,在ILSVRC&COCO 2015比赛中进一步获得了以下任务的第一名:ImageNet检测,ImageNet定位,COCO检测和COCO分割。这些强有力的证据表明残差学习原则是通用的,并且作者期望残差结构适用于其他视觉和非视觉问题(事后证明,残差结构是一种通用且高效的结构,在目标检测和图像分割上都有使用,而且Resnet这种跳跃式连接也启发其他一些好模型的诞生)

正常来说接下来看总结,但是作者把总结和引言这两部分一起写,那么就接下来就看看Resnet的结构

模型结构

残差学习

将H(x)视为一个待拟合的底层映射,可以由一些堆叠的层(不一定是整个网络)来逼近,其中x表示这些层的第一层的输入。如果假设多个非线性层可以渐近地逼近复杂的函数,那么等同于假设它们可以渐近地逼近残差函数,即H(x) - x(假设输入和输出具有相同的维度)。因此,与其期望堆叠的层逼近H(x),明确地让这些层逼近一个残差函数F(x) := H(x) - x。因此,原始函数变为F(x) + x。虽然两种形式都应该能够渐近地逼近所需的函数(正如我们所假设的那样),但学习的便利性可能会有所不同。

这种改写是由对退化问题的反直觉现象所驱动的。正如在引言中讨论的,如果添加的层可以构造为恒等映射,那么深度模型的训练误差不应该大于其相对较浅的对应物。退化问题表明,求解器可能难以通过多个非线性层逼近恒等映射。通过残差学习的重新表述,如果恒等映射是最优的,求解器可以简单地将多个非线性层的权重推向零以逼近恒等映射。

在实际情况下,恒等映射不太可能是最优的,但论文的改写可能有助于预处理问题。如果最优函数更接近于恒等映射而不是零映射,那么对于求解器来说,相对于恒等映射的参考来找到扰动应该更容易,而不是学习函数作为新函数。论文通过实验证明学到的残差函数通常具有较小的响应,这表明恒等映射提供了合理的预处理。

跳跃连接的恒等映射

 这里看着有点复杂,其实是对残差结构的进一步说明,简单来说,在维度相同的情况下(就是F(x)和x的尺寸大小和通道数相同的),F(x)与x可以直接相加,在维度不相同的情况下,F(x)卷积下采样后,尺寸减半,通道数翻倍,而x的由于短接的作用维度不变,因为需要一次下采样,与F(x)的维度保持一样,这个下采样被称为跳跃连接的恒等映射

图2 

论文对每几个堆叠的层采用残差学习。如图2所示,声明一个定义为: y=F(x,{Wi​})+x. 这里,x 和 y 分别是所考虑层的输入和输出向量。函数F(x,{Wi​}) 表示要学习的残差映射。对于图2中具有两个层的示例,F=W_{2}\sigma (W_{1}x),其中 σ 表示ReLU,为简化符号省略了偏置。操作 F+x 由跳跃连接和逐元素加法执行。我们在加法之后采用第二个非线性。

y=F(x,{Wi​})+x中的跳跃连接既不引入额外的参数也不引入计算复杂性。这不仅在实践中具有吸引力,而且在普通网络和残差网络之间进行比较时也非常重要。我们可以公平地比较具有相同数量的参数、深度、宽度和计算成本(除了可以忽略的逐元素加法之外)的普通/残差网络。

在y=F(x,{Wi​})+x中,x 和 F 的维度必须相等。如果不相等(例如,当更改输入/输出通道时),我们可以通过快捷连接执行线性投影 Ws 来匹配维度:y=F(x,{Wi​})+Ws​x.(就是下采样之后保持维度相同)

在y=F(x,{Wi​})+x中也可以使用方阵 Ws。但我们将通过实验证明,对于解决退化问题和经济性而言,恒等映射已经足够,因此只有在需要匹配维度时才使用 Ws

残差函数 F 的形式是灵活的。本文的实验证明了一个具有两个或三个层的 F 函数(图5),但更多层也是可能的。但如果 F 只有一个单一层,Eqn.(1)类似于线性层:y=W1​x+x, 我们还注意到,尽管上述符号是为了简化而关于全连接层的,但它们也适用于卷积层。函数 F(x,{Wi​}) 可以表示多个卷积层。逐通道执行逐元素加法,而不是矩阵加法。

 网络结构

Resnet的网络连接特别简单,像VGG网络一样一层层堆卷积层,但是,在每两层之间插入了跳跃连接。当输入和输出的维度相同时(实线快捷连接),可以直接使用恒等跳跃连接(y=F(x,{Wi​})+x)。当维度不相同(虚线快捷连接),我们考虑两个选项:(A)快捷连接仍然执行恒等映射,为增加的维度填充额外的零条目。此选项不引入额外的参数;(B)使用y=F(x,{Wi​})+Ws​x中的投影快捷连接来匹配维度(通过1×1卷积完成)。对于这两个选项,当快捷连接跨越两个不同尺寸的特征图时,它们的执行采用步幅为2。

下面就是VGG19与Resnet34的模型对比,中间为Resnet的平面模型,就是不加跳跃连接的模型,最右就为Resnet34完整模型

可以发现,其实VGG19何Resnet34基本上是一样的,也就Resnet34比VGG19多了个残差连接,但是这个残差连接带来的效果是巨大的,解决了梯度爆炸和梯度消失的问题,可以参考下面的梯度更新的公式:\frac{\partial y}{\partial x}=\frac{\partial F(x)}{\partial x}+1,相当于一直保留一个1,就解决了深度带来的梯度的问题

 

实验 

Resnet有以下这几种结构

而且在实验中,论文分有无残差结构的模型进行实验,验证残差结构的作用

上图就表示为无残差结构与带残差结构的Resnet18、Resnet34模型在ImageNet数据集上的表现,可以发现,在不带残差结构的情况下,34层的效果比18层的效果要差

但是在使用残差结构之后,深层的神经网络能够充分发挥本身的性能,识别效果比18层优异得多,直接证明了残差结构的作用

上图是Resnet与前面网络的性能对比,可以发现,Resnet的性能最好,而且Resnet也证明了,网络层数越深,效果越好(结论具有局限性,目前的论文已经证明了卷积神经网络会达到饱和,在大型数据集上表现不如Transformer)

上图为Resnet不同层数在CIFAR10数据集上的表现

Resnet的模型搭建

 前面对Resnet模型进行很详细的讲解了,而且Resnet与VGG系列的模型结构大差不差,那么我这里就直接放出Resnet的模型代码了

import torch.nn as nn
import torch


class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
                               kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channel)
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
                               kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channel)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        out += identity
        out = self.relu(out)

        return out


class Bottleneck(nn.Module):
    """
    注意:原论文中,在虚线残差结构的主分支上,第一个1x1卷积层的步距是2,第二个3x3卷积层步距是1。
    但在pytorch官方实现过程中是第一个1x1卷积层的步距是1,第二个3x3卷积层步距是2,
    这么做的好处是能够在top1上提升大概0.5%的准确率。
    可参考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
    """
    expansion = 4

    def __init__(self, in_channel, out_channel, stride=1, downsample=None,
                 groups=1, width_per_group=64):
        super(Bottleneck, self).__init__()

        width = int(out_channel * (width_per_group / 64.)) * groups

        self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width,
                               kernel_size=1, stride=1, bias=False)  # squeeze channels
        self.bn1 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,
                               kernel_size=3, stride=stride, bias=False, padding=1)
        self.bn2 = nn.BatchNorm2d(width)
        # -----------------------------------------
        self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel*self.expansion,
                               kernel_size=1, stride=1, bias=False)  # unsqueeze channels
        self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample is not None:
            identity = self.downsample(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        out += identity
        out = self.relu(out)

        return out


class ResNet(nn.Module):

    def __init__(self,
                 block,
                 blocks_num,
                 num_classes=1000,
                 include_top=True,
                 groups=1,
                 width_per_group=64):
        super(ResNet, self).__init__()
        self.include_top = include_top
        self.in_channel = 64

        self.groups = groups
        self.width_per_group = width_per_group

        self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
                               padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channel)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, blocks_num[0])
        self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
        self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
        self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
        if self.include_top:
            self.avgpool = nn.AdaptiveAvgPool2d((1, 1))  # output size = (1, 1)
            self.fc = nn.Linear(512 * block.expansion, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')

    def _make_layer(self, block, channel, block_num, stride=1):
        downsample = None
        if stride != 1 or self.in_channel != channel * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(channel * block.expansion))

        layers = []
        layers.append(block(self.in_channel,
                            channel,
                            downsample=downsample,
                            stride=stride,
                            groups=self.groups,
                            width_per_group=self.width_per_group))
        self.in_channel = channel * block.expansion

        for _ in range(1, block_num):
            layers.append(block(self.in_channel,
                                channel,
                                groups=self.groups,
                                width_per_group=self.width_per_group))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        if self.include_top:
            x = self.avgpool(x)
            x = torch.flatten(x, 1)
            x = self.fc(x)

        return x


def resnet34(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet34-333f7ec4.pth
    return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)


def resnet50(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet50-19c8e357.pth
    return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)


def resnet101(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
    return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)


def resnext50_32x4d(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth
    groups = 32
    width_per_group = 4
    return ResNet(Bottleneck, [3, 4, 6, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)


def resnext101_32x8d(num_classes=1000, include_top=True):
    # https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth
    groups = 32
    width_per_group = 8
    return ResNet(Bottleneck, [3, 4, 23, 3],
                  num_classes=num_classes,
                  include_top=include_top,
                  groups=groups,
                  width_per_group=width_per_group)

下面就是CIFAR10的训练代码,承接VGG16模型,我这里的模型选择的是Resnet34,上面的模型带预训练权重的下载链接,也可以载入预训练权重进行训练

import numpy as np
import torch
import torchvision
import argparse
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from torchvision.transforms import InterpolationMode
from alexnet import AlexNet
from VGG_net import make_VGG16
from Resnet import resnet34
import ssl

# 全局取消证书验证
ssl._create_default_https_context = ssl._create_unverified_context

device= torch.device('cuda' if torch.cuda.is_available() else 'cpu')# gpu是否可以用
print(device)

def train(args):
    # 超参数设置,方便管理
    num_epochs = args.max_epoch
    batch_size = args.batch_size
    learning_rate = args.lr
    image_size = args.image_size
    momentum = args.momentum
    # 设置数据集的格式
    transform = transforms.Compose([transforms.Resize((image_size, image_size),
                                    interpolation=InterpolationMode.BICUBIC),
                                    transforms.ToTensor(),
                                    transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.247, 0.2435, 0.2616])
                                    ])
    # 数据加载
    # 如果没有这个数据集的话会自动下载
    train_data = torchvision.datasets.CIFAR10(root="dataset",download=True,transform=transform,train=True)
    test_data = torchvision.datasets.CIFAR10(root="dataset",download=True,transform=transform,train=False)

    train_dataloader = DataLoader(train_data, batch_size=batch_size)
    test_dataloader = DataLoader(test_data, batch_size=batch_size)
    print('Dataload is Ready')
    # 添加tensorboard路径
    writer = SummaryWriter(log_dir=args.SummerWriter_log)
    # 模型加载
    # model = make_VGG16(num_classes=10,path=r'./vgg161.pth',use_BN=False).to(device)# 注意,这里是False,也就是不使用BN正则化,论文中的VGG16的是没有BN正则化的
    # model = AlexNet(num_classes=10).to(device)
    model = resnet34(num_classes=10).to(device)
    # 参数量估计
    total = sum([param.nelement() for param in model.parameters()])
    print("Number of parameters: %.2fM" % (total / 1e6))
    # Loss and optimizer
    # 选择交叉熵作为损失函数
    criterion = nn.CrossEntropyLoss()
    # 选择SGD为优化器
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)
    total_train_step = 0#记录训练次数
    total_test_step=0#记录测试次数


    # 开始训练
    for epoch in range(num_epochs):
        print("---------------第{}轮训练开始-------------".format(epoch + 1))
        for i, (images, labels) in enumerate(train_dataloader):
            images = images.to(device)
            labels = labels.to(device)

            # Forward pass
            outputs = model(images)
            loss = criterion(outputs, labels)

            # Backward and optimize
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_train_step = total_train_step + 1

            if (i + 1) % args.print_map_epoch == 0:# 100次显示一次loss
                print("Epoch [{}/{}], Step [{}] Loss: {:.4f}"
                          .format(epoch + 1, num_epochs, total_train_step, loss.item()))
            writer.add_scalar("train_loss", loss.item(), total_train_step)

        # Test the model
        model.eval()
        total_test_loss = 0
        total_accuary = 0  # 正确率
        with torch.no_grad():
            correct = 0
            total = 0
            for images, labels in test_dataloader:
                images = images.to(device)
                labels = labels.to(device)
                outputs = model(images)
                loss = criterion(outputs, labels)
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()
                total_test_loss += loss
                total_accuary += correct
            print('Accuracy of the model on the test images: {} %'.format(100 * correct / total))
            writer.add_scalar("test_loss",total_test_loss,total_test_step)
            writer.add_scalar("test_accuary", correct / total, total_test_step)
            total_test_step += 1
    # Save the model checkpoint
    torch.save(model, 'weights.pth')




if __name__ == "__main__":
    parser = argparse.ArgumentParser()

    '''------------------------------------------调  节  部  分------------------------------------------------------'''
    parser.add_argument('--max_epoch', type=int, default=30, help='total epoch')
    parser.add_argument('--device_num', type=str, default='cpu', help='select GPU or cpu')
    parser.add_argument('--image_size', type=int, default=224, help='if crop img, img will be resized to the size')
    parser.add_argument('--batch_size', type=int, default=16, help='batch size, recommended 16')
    parser.add_argument('--lr', type=float, default=0.007, help='learning rate')
    parser.add_argument('--momentum', type=float, default=0.90, help='choice a float in 0-1')
    parser.add_argument('--print_map_epoch', type=int, default=100, help='')
    parser.add_argument('--SummerWriter_log', type=str, default='Resnet', help='')

    args = parser.parse_args()

    train(args)






训练结果

下图是训练集的损失变化图(忽略那条直线,没改tensorboard文件夹路径导致的),可以发现快速就下降到损失1,并且基本稳定在损失为0.4的位置

下图是测试集的损失变化曲线和准确率曲线,在第6个epoch就基本稳定

结语

Resnet模型的搭建和论文简读就在这里就结束了,对Resnet有兴趣的朋友可以更层数或者在VGG网络上添加残差结构来训练看看最终的训练结果。

这里我只是对Resnet论文进行了简读,会遗漏原文中的细节,有兴趣的朋友最好还是去阅读原文,每个人的理解是不相同的,相信你阅读原文会有自身的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值