【深度学习】ResNet残差网络 ResidualBlock残差块实现(pytorch)

2023-11-19


前言

这两天为了搞硕士论文课题的创新点,在网上找了大量的开源项目代码进行实验,但是很可惜每次跑完demo之后就不知道干啥了(主要还是练习少了,很多代码看不董,不知道为何要这么用),归根结底还是自己在深度学习的基础代码上面的知识学的很不扎实(尤其是构建网络这些,应该自己取搭建一下)。所以趁着距离开题还有1个月,我准备返璞归真,取把一些基础的深度学习代码自己复现一下。

本期主要复现resnet18这个网路,以及残差块ResidualBlock的网络结构(pytorch),看看它们到底是怎么运作的,学习一下。

本期代码:完全参考B站上 跟着李沐学AI的视频:
参考视频
李沐老师的教材:https://zh-v2.d2l.ai/chapter_convolutional-modern/resnet.html#id4
我也相当于是跟着老师的视频自己敲了一遍

一、卷积的相关计算公式(复习)

在此之前呢,我也已经把卷积的具体计算公式遗忘的差不多了(尤其是加入了padding,strade这些参数),所以这里我稍微复习一下:
对于长和宽来说:
经过卷积后的尺寸=(输入尺寸-卷积核尺寸+2×padding)/步长+1

这里我们来做点测试:

import torch
from torch import nn
#高宽保持不变的3x3卷积
X=torch.rand(4,3,512,512)
print(X.shape)
conv2=nn.Conv2d(in_channels=3,out_channels=3,kernel_size=3,stride=1,padding=1,)
Y=conv2(X)
print(Y.shape)

在这里插入图片描述
这里可以看到高宽都没变,这是一个经典的卷积核结构(311)
我们再试一个(步长改为2):

import torch
from torch import nn
#高宽减半的3x3卷积
X=torch.rand(4,3,512,512)
print(X.shape)
conv2=nn.Conv2d(in_channels=3,out_channels=3,kernel_size=3,stride=2,padding=1,)
Y=conv2(X)
print(Y.shape)

在这里插入图片描述
在这里,高宽都变为2了,这也是经典的结构(321)

顺带一提,改变通道数就很简单了,直接写出输入的通道数,输出想要的通道数:

import torch
from torch import nn

X=torch.rand(4,3,512,512)
print(X.shape)
conv2=nn.Conv2d(in_channels=3,out_channels=64,kernel_size=3,stride=1,padding=1,)
Y=conv2(X)
print(Y.shape)

在这里插入图片描述
如图所示通道数从3变成了64

二、残差块ResidualBlock复现(pytorch)

在老师的视频里面,一共提到了两种作用的残差块:
1)高宽减半(h,w),通道数翻倍。这是因为卷积的步长设为了2。
2)高宽不变,通道数也不变。这是因为卷积的步长设为了1
在这里插入图片描述
这个图也是抄的老师书上的。
具体的实现过程代码如下(老师视频里的):

import torch
from torch import nn
import torch.nn.functional as F  #forward函数里面会用到F.relu()
class ResidualBlock(nn.Module): #M一定要大写,这是一个经常烦的错误
    #构造方法(构造函数中至少需要传入2个参数:进出的通道数。残差块的一个最主要的作用就是改变信号的通道数)
    def __init__(self,in_channles,num_channles,use_1x1conv=False,strides=1): #第三个参数是是否使用1x1卷积
        super(ResidualBlock, self).__init__()
        self.conv1 = nn.Conv2d(
            in_channles,num_channles,kernel_size=3,stride=strides,padding=1,) #默认为311结构 宽高不会变,但把步长改为2,就会变
        self.conv2 = nn.Conv2d(
            num_channles, num_channles, kernel_size=3, padding=1)  #默认这里宽高也不会变,但把步长改为2,就会变
        if use_1x1conv:
            self.conv3=nn.Conv2d(
                in_channles,num_channles,kernel_size=1,stride=strides)  #这里相当于就是残差连接了
        else:
            self.conv3=None
        self.bn1=nn.BatchNorm2d(num_channles) #批归一化
        self.bn2=nn.BatchNorm2d(num_channles)
        self.relu=nn.ReLU(inplace=True)   #节省内存
    def forward(self,x):
        y= F.relu(self.bn1(self.conv1(x)))
        y=self.bn2(self.conv2(y))
        if self.conv3:
            x=self.conv3(x)
        y+=x
        return F.relu(y)  #在forward里的relu是这样调用的

本来想自己复现的,结果发现运行出来一直有问题,有时间我会补在下面。
我们再来看看上面的代码:
1)use_conv3为ture时,会执行宽高减半,通道数增倍的操作,属于是第一种残差块。
2)use_conv3为false时,会执行宽高不变,通道数不变的操作,属于是第二种残差块。
3)forward里面的 y+=x相当于实现了残差连接的作用。这里的x也有两种情况(后面的if能体现出来):原本输入到残差块里的x,以及高宽减半通道数增倍的x。所以要实现残差连接还是很简单的,直接在forward里添加上最开始的输入就行。
4)残差块里的conv1和conv2不会改变输入的高宽(经典311结构,311是不会改变高宽的)
5)forward里使用Relu是从F中使用,self.relu更多用在构建网络结构上,F.relu用在forward上。

三、残差网络ResNet18复现(pytorch)

首先我们看看论文里的resnet18的结构(值得一提的是,这里的2个小残差块合成了更大的残差块):

在这里插入图片描述

如果还要看更高清的网络结构,可以在网上找,我这里就不偷了。
整个网络结构解读如下:
1)conv1:最开始输入是3x224x224,conv1是64个7x7,步长为2的卷积块,经过之后输出为64x112x112;步长为2的3x3最大池化,输出为 64x56x56。
2)conv2:由两个高宽不变的残差块构成输出是64x56x56 (相当于 第二部分的第一个残差块是个异端,跟后面不太一样)
3)conv3:由一个高宽减半,通道数增倍的残差块,以及一个高宽不变的残差块构成输出是128x28x28
4)conv4:由一个高宽减半,通道数增倍的残差块,以及一个高宽不变的残差块构成输出是256x14x14
5)conv5:由一个高宽减半,通道数增倍的残差块,以及一个高宽不变的残差块构成输出是512x7x7
6)剩下的部分:一个池化,一个全连接层
紧接着我们来一步步定义

#搭建ResNet18网络
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))
def ResidualBlock_big(input_channels, num_channels, num_residuals,
                 first_block=False): #参数分别是进出的通道数,小残差快的个数,判断是否为第一个大残差块
    blk = []
    for i in range(num_residuals):  #高宽减半,通道数加倍
        if i == 0 and not first_block:
            blk.append(ResidualBlock(input_channels, num_channels,
                                use_1x1conv=True, strides=2))
        else:
            blk.append(ResidualBlock(num_channels, num_channels)) #如果是第一大残差快的小残差块,不需要让高宽减半
    return blk

b2 = nn.Sequential(*ResidualBlock_big(64, 64, 2, first_block=True))  #判断是不是4个残差快中的第1个,第1个残差快不用做减半(因为前面已经减半很多了)
b3 = nn.Sequential(*ResidualBlock_big(64, 128, 2))
b4 = nn.Sequential(*ResidualBlock_big(128, 256, 2))
b5 = nn.Sequential(*ResidualBlock_big(256, 512, 2))
ResNet18 = nn.Sequential(b1, b2, b3, b4, b5,
                    nn.AdaptiveAvgPool2d((1,1)),
                    nn.Flatten(), nn.Linear(512, 10))  #剩下三层

这里def函数 用来定义大的残差块,前面我们也提到有一个异端的残差块,所以这里的if else就是来判断它的,最后返回的是一个列表,后面的b2 = nn.Sequential(*ResidualBlock_big(64, 64, 2, first_block=True)) 里面的 *是把列表里面的内容取出来的意思,我们看看就知道为什么了:

import torch
from torch import nn
import torch.nn.functional as F
class ResidualBlock(nn.Module): #M一定要大写,这是一个经常烦的错误
    #构造方法(构造函数中至少需要传入2个参数:进出的通道数。残差块的一个最主要的作用就是改变信号的通道数)
    def __init__(self,in_channles,num_channles,use_1x1conv=False,strides=1):
        super(ResidualBlock, self).__init__()
        self.conv1 = nn.Conv2d(
            in_channles,num_channles,kernel_size=3,stride=strides,padding=1,)
        self.conv2 = nn.Conv2d(
            num_channles, num_channles, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3=nn.Conv2d(
                in_channles,num_channles,kernel_size=1,stride=strides)
        else:
            self.conv3=None
        self.bn1=nn.BatchNorm2d(num_channles)
        self.bn2=nn.BatchNorm2d(num_channles)
        self.relu=nn.ReLU(inplace=True)
    def forward(self,x):
        y= F.relu(self.bn1(self.conv1(x)))
        y=self.bn2(self.conv2(y))
        if self.conv3:
            x=self.conv3(x)
        y+=x
        return F.relu(y)
blk=[]
blk.append(ResidualBlock(64, 64,use_1x1conv=False, strides=1))
blk.append(ResidualBlock(64, 128,use_1x1conv=True, strides=2))
print(blk)

我们打印一下结构:
在这里插入图片描述
如果在前面加一个* :
在这里插入图片描述
最后老师的教材给出了一种展示网络输出的方法:

X = torch.rand(size=(1, 1, 224, 224))
#打印网络结构
print(ResNet18)
for layer in ResNet18:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

在这里插入图片描述
我们也print打印展示一下(展示部分):
在这里插入图片描述

当然这个结构感觉有点乱,有时间我还是要自己复现一下

四、直接调用方法

实现完毕后,感觉自己的基础知识又夯实了一些,为以后的网络架构创新新添了动力。
当然一般情况下都是直接调用resnet18使用,更方便些:

resnet18=models.resnet18(progress=True)

五、具体实践(ResNet进行猫狗分类)

文件夹我们要弄好(猫狗的数据集):
在这里插入图片描述

我们首先进行数据加载:

import torch
import torch.nn as nn
import hiddenlayer as hl
import torchvision.datasets as datasets
from torchvision import transforms
from torchvision import models
import torch.utils.data as Data
from tqdm import tqdm
transforms=transforms.Compose([
    transforms.RandomResizedCrop(512),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])
])
train_data = datasets.ImageFolder('data/train',transform=transforms)
test_data=datasets.ImageFolder('data/test',transform=transforms)
print("训练集共有图像{}张".format(len(train_data.imgs)))
print("测试集共有图像{}张".format(len(test_data.imgs)))
train_data_loader=Data.DataLoader(train_data,batch_size=32,shuffle=True,num_workers=0)
test_data_loader=Data.DataLoader(test_data,batch_size=32,shuffle=True,num_workers=0)

在这里插入图片描述
我们查看一下标签b_y长啥样(imagefolder会自动生成标签):

for step, (b_x, b_y) in enumerate(train_data_loader):
    if step >0:
        break
    print(b_y)
    print(len(b_y))  #len(b_y)就是batch_size

在这里插入图片描述
紧接着定义网络:

resnet18=models.resnet18(progress=True).to(device)

设置学习率等参数:

LR = 0.003
optimizer = torch.optim.Adam(resnet18.parameters(), lr=LR) #优化器
loss_func = nn.CrossEntropyLoss()  # 损失函数,这里是多分类问题,可以用交叉熵

训练过程:

# 记录训练过程的指标
history1 = hl.History()
# 使用Canvas进行可视化
canvas1 = hl.Canvas()
train_num = 0
val_num = 0

## 对模型进行迭代训练,对所有的数据训练EPOCH轮
for epoch in range(10):
    train_loss_epoch = 0
    val_loss_epoch = 0
    train_corrects=0
    val_corrects=0
    ## 对训练数据的迭代器进行迭代计算
    loop1 = tqdm(enumerate(train_data_loader), total=len(train_data_loader) - 1)
    for step, (b_x, b_y) in loop1:
        b_x=b_x.to(device) #数据传到显卡上
        b_y = b_y.to(device)
        resnet18.train()
        ## 使用每个batch进行训练模型
        output = resnet18(b_x)
        loss = loss_func(output, b_y)


        optimizer.zero_grad()  # 每个迭代步的梯度初始化为0
        loss.backward()  # 损失的后向传播,计算梯度
        optimizer.step()  # 使用梯度进行优化
        train_loss_epoch += loss.item() * b_x.size(0)
        train_num = train_num + b_x.size(0)
        loop1.set_description(f'Epoch [{epoch}/{10 - 1}]')
        loop1.set_postfix(train_loss=train_loss_epoch / train_num)
    train_loss = train_loss_epoch / train_num


    loop2 = tqdm(enumerate(test_data_loader), total=len(test_data_loader) - 1)
    ## 使用每个batch进行验证模型
    for step, (b_x, b_y) in loop2:
        b_x = b_x.to(device) # 数据传到显卡上
        b_y = b_y.to(device)
        resnet18.eval()
        output = resnet18(b_x)
        loss = loss_func(output, b_y)
        val_loss_epoch += loss.item() * b_x.size(0)
        val_num = val_num + b_x.size(0)
    ## 计算一个epoch的损失


        loop2.set_description(f'Epoch [{epoch}/{10 - 1}]')
        loop2.set_postfix(val_loss = val_loss_epoch / val_num)
    val_loss = val_loss_epoch / val_num
    ## 保存每个epoch上的输出loss
    history1.log(epoch, train_loss=train_loss,
                 val_loss=val_loss)
    # 可视网络训练的过程
    with canvas1:
        canvas1.draw_plot([history1["train_loss"], history1["val_loss"]])

在这里插入图片描述
在这里插入图片描述
可以看出,loss下降的很快,我这里只给出train的过程,test没写了,也很简单

六.可能报错

6.1.TypeError: init() takes 2 positional arguments but 4 were given

这里报错是因为使用transforms.Compose([时,没有打最里面的那个括号【】

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【深度学习】ResNet残差网络 ResidualBlock残差块实现(pytorch) 的相关文章

  • PyTorch:tensor.cuda()和tensor.to(torch.device(“cuda:0”))之间有什么区别?

    在 PyTorch 中 以下两种将张量 或模型 发送到 GPU 的方法有什么区别 Setup X np array 1 3 2 3 2 3 5 6 1 2 3 4 X model X torch DoubleTensor X Method
  • 推导 pytorch 网络的结构

    对于我的用例 我需要能够采用 pytorch 模块并解释模块中的层序列 以便我可以以某种文件格式在层之间创建 连接 现在假设我有一个简单的模块 如下所示 class mymodel nn Module def init self input
  • pytorch - “conv1d”在哪里实现?

    我想看看 conv1d 模块是如何实现的https pytorch org docs stable modules torch nn modules conv html Conv1d https pytorch org docs stabl
  • PyTorch - 参数不变

    为了了解 pytorch 的工作原理 我尝试对多元正态分布中的一些参数进行最大似然估计 然而 它似乎不适用于任何协方差相关的参数 所以我的问题是 为什么这段代码不起作用 import torch def make covariance ma
  • 运行时错误:CUDA 错误:设备端断言已触发 - 训练 LayoutLMV3 时

    我正在训练最新版本的layoutLMv3模型 但在开始训练时trainer train 出现以下错误 请帮我解决它 我使用的是 v100 4 GPU RuntimeError Traceback most recent call last
  • 在pytorch中使用tensorboard,但得到空白页面?

    我在pytorch 1 3 1中使用tensorboard 并且我在张量板的 pytorch 文档 https pytorch org docs stable tensorboard html 运行后tensorboard logdir r
  • torch-1.1.0-cp37-cp37m-win_amd64.whl 在此平台上不受支持的滚轮

    我在开发 RNN 时需要使用 pyTorch 每当我尝试安装它时 我都会收到一条错误消息 指出 torch 1 1 0 cp37 cp37m win amd32 whl 在此平台上不受支持 pip3安装https download pyto
  • 在 PyTorch 中原生测量多类分类的 F1 分数

    我正在尝试在 PyTorch 中本地实现宏 F1 分数 F measure 而不是使用已经广泛使用的sklearn metrics f1 score https scikit learn org stable modules generat
  • 在pytorch张量中过滤数据

    我有一个张量X like 0 1 0 5 1 0 0 1 2 0 我想实现一个名为的函数filter positive 它可以将正数据过滤成新的张量并返回原始张量的索引 例如 new tensor index filter positive
  • 从打包序列中获取每个序列的最后一项

    我试图通过 GRU 放置打包和填充的序列 并检索每个序列最后一项的输出 当然我的意思不是 1项目 但实际上是最后一个 未填充的项目 我们预先知道序列的长度 因此应该很容易为每个序列提取length 1 item 我尝试了以下方法 impor
  • pytorch 中的 autograd 可以处理同一模块中层的重复使用吗?

    我有一层layer in an nn Module并在一次中使用两次或多次forward步 这个的输出layer稍后输入到相同的layer pytorch可以吗autograd正确计算该层权重的梯度 def forward x x self
  • BatchNorm 动量约定 PyTorch

    Is the 批归一化动量约定 http pytorch org docs master modules torch nn modules batchnorm html 默认 0 1 与其他库一样正确 例如Tensorflow默认情况下似乎
  • 如何更新 PyTorch 中神经网络的参数?

    假设我想将神经网络的所有参数相乘PyTorch 继承自的类的实例torch nn Module http pytorch org docs master nn html torch nn Module by 0 9 我该怎么做呢 Let n
  • Pytorch GPU 使用率低

    我正在尝试 pytorch 的例子https pytorch org tutorials beginner blitz cifar10 tutorial html https pytorch org tutorials beginner b
  • pytorch 的 IDE 自动完成

    我正在使用 Visual Studio 代码 最近尝试了风筝 这两者似乎都没有 pytorch 的自动完成功能 这些工具可以吗 如果没有 有人可以推荐一个可以的编辑器吗 谢谢你 使用Pycharmhttps www jetbrains co
  • PyTorch 中的连接张量

    我有一个张量叫做data形状的 128 4 150 150 其中 128 是批量大小 4 是通道数 最后 2 个维度是高度和宽度 我有另一个张量叫做fake形状的 128 1 150 150 我想放弃最后一个list array从第 2 维
  • PyTorch 中的交叉熵

    交叉熵公式 但为什么下面给出loss 0 7437代替loss 0 since 1 log 1 0 import torch import torch nn as nn from torch autograd import Variable
  • 如何使用 pytorch 同时迭代两个数据加载器?

    我正在尝试实现一个接收两张图像的暹罗网络 我加载这些图像并创建两个单独的数据加载器 在我的循环中 我想同时遍历两个数据加载器 以便我可以在两个图像上训练网络 for i data in enumerate zip dataloaders1
  • 在Pytorch中计算欧几里得范数..理解和实现上的麻烦

    我见过另一个 StackOverflow 线程讨论计算欧几里德范数的各种实现 但我很难理解特定实现的原因 如何工作 该代码可以在 MMD 指标的实现中找到 https github com josipd torch two sample b
  • 样本()和r样本()有什么区别?

    当我从 PyTorch 中的发行版中采样时 两者sample and rsample似乎给出了类似的结果 import torch seaborn as sns x torch distributions Normal torch tens

随机推荐

  • Pytorch学习笔记(二)

    后续遇到一些函数等知识 还会进行及时的补充 tensor的创建 使用pytorch中的列表创建tensor tensor torch Tensor 1 1 0 2 print
  • python 基础

    条件表达式 if 1 gt 2 print 111 elif 2 gt 3 print 222 else print 333 for循环 for i in range 3 10 print i 输出结果 for循环 步长为2 for i i
  • QStyle 自定义QSpinBox外观

    点击查看详细介绍 头文件 ifndef SPINBOX STYLE 1 H define SPINBOX STYLE 1 H include
  • 【高等数学基础知识篇】——函数,极限与连续

    本文仅用于个人学习记录 使用的教材为汤家凤老师的 高等数学辅导讲义 本文无任何盈利或者赚取个人声望的目的 如有侵权 请联系删除 文章目录 一 函数基础知识 1 1 基本初等函数和初等函数 1 2 函数的初等特性 1 3 特殊函数 二 函数题
  • PageHelper的order by方法可替代mybatis中order by必须使用$来避免sql注入

    PageHelper的order by方法可替代mybatis中order by必须使用 来避免sql注入 在my batis中 我们通常使用 字符 来传值 在mybatis中使用order by排序时也习惯性的使用 然后发现sql错误 后
  • 【数学建模笔记 24】数学建模的时间序列模型

    24 时间序列模型 定义 时间序列是按时间顺序排列的 随时间变化且相互关联的数据序列 分析时间序列的方法构成数据分析的一个重要领域 即时间序列分析 一个时间序列往往是以下几类变化形式的叠加 长期趋势变动 T t T t Tt 朝一定方向的变
  • zabbix 通过import批量导入新增主机和批量删除旧的主机

    通过import批量导入新增主机 本文采用zabbix的hosts页面的import 批量导入 zabbix3 2版本批量导入模板 bin bash filename zbx xml echo
  • 『sklearn学习』多种模型预测脸的下半部分的结果对比

    预测脸的下半部分 import numpy as np import matplotlib pyplot as plt from sklearn datasets import fetch olivetti faces from sklea
  • Opencv载取任意长度视频

    文章目录 使用Opencv截取仍意长度视频 使用Opencv截取仍意长度视频 import cv2 import sys def select video input path output path start 1 end 1 input
  • 用Flask搭建一个web应用(三)---拆分models.py&解决循环引用

    在app py同级目录下建立models py models py from flask sqlalchemy import SQLAlchemy from app import db class Article db Model 定义表名
  • Redis缓存详解 -- 转载

    Redis缓存详解 一 缓存穿透 二 缓存雪崩 三 缓存击穿 本篇为转载 只做码届搬运工 Thanks 一 缓存处理流程 前台请求 后台先从缓存中取数据 取到直接返回结果 取不到时从数据库中取 数据库取到更新缓存 并返回结果 数据库也没取到
  • 浏览器发器POST请求

    浏览器按F12或打开开发者工具 在console 控制台 标签页下输入 fetch new Request http localhost 8080 power font getToken method POST headers Conten
  • oracle同比计算

    计算同地区下 同比百分比 select 2020 area no area desc city no city desc area level key id CASE WHEN NVL SUM VAL 19 0 0 THEN 0 ELSE
  • word2vec损失函数

    未优化前损失函数 以CBOW为例 利用softmax层计算出字典V中每个词的概率 再构建交叉熵损失函数 负采样损失函数 直接对词典里的V个词计算相似度并归一化显然是极其耗时的 为此作者提出了层次Softmax和负采样两种损失层 负采样损失函
  • 【笔试】操作系统知识点整理

    一 操作系统概述 1 操作系统的主要功能 进程与处理机管理 作业和进程调度 进程控制和进程通信 存储管理 内存分配 地址映射 内存保护和内存扩充 设备管理 缓冲区管理 设备分配 设备驱动 设备无关性 文件管理 文件存储空间的管理 文件操作的
  • Eclipse 安装阿里巴巴代码规范插件的步骤

    2017年10月14日杭州云栖大会 Java代码规约扫描插件全球首发仪式正式启动 规范正式以插件形式公开走向业界 引领Java语言的规范之路 目前 插件已在云效公有云产品中集成 立即体验 云效 gt 公有云 gt 设置 gt 测试服务 gt
  • c++系列 —— 移动构造函数

    往期地址 c 系列一 c 的封装 c 系列二 c 的继承 c 系列三 继承和多态特性 c 系列四 运算符重载 c 系列五 静态成员和静态类 c 系列六 友元函数和友元类 c 系列七 STL编程之模板template c 系列八 STL编程之
  • 事件循环机制分享

    Event Loop 即事件循环 是JavaScript或Node为解决单线程代码执行不阻塞主进程一种机制 也就是我们所说的异步原理 要了解事件循环机制首先要了解进程 线程 宏任务 微任务 进程 Process 是计算机中的程序关于某数据集
  • 【STM32F0】Keil 查看局部变量显示

    现象 在进行STM32F0开发的时候出现了 调试代码 添加变量Watch时 显示not in scope 处理方式 因为代码开了优化的处理 把优化改到Level0 就可以解决问题
  • 【深度学习】ResNet残差网络 ResidualBlock残差块实现(pytorch)

    文章目录 前言 一 卷积的相关计算公式 复习 二 残差块ResidualBlock复现 pytorch 三 残差网络ResNet18复现 pytorch 四 直接调用方法 五 具体实践 ResNet进行猫狗分类 六 可能报错 6 1 Typ