PyTorch深度学习快速入门【小土堆】

目录

1.Python学习中的两大法宝函数

2. Pycharm及Jupyter使用及对比

如何在PyCharm中新建项目?

如何在Jupyter中新建项目?

三种运行方式(PyCharm、PyCharm的Python控制台、Jupyter Notebook)对比:

3. PyTorch加载数据初认识

PyTorch 读取数据涉及两个类:Dataset & Dataloader

数据集的几种组织形式

Dataset类

4. Dataset类代码实战

如何读取图片

绝对路径和相对路径:

数据格式一

数据格式二

5. TensorBoard的使用

SummaryWriter类

主要用到两个方法:

add_scalar() 方法的使用

title 和 y 不一致的情况:

解决:

add_image() 的使用

利用numpy.array(),对PIL图片进行转换

6. torchvision中transforms的使用

transforms的结构及用法

结构

一些类

1.Transforms该如何使用:

2.Tensor数据类型与其他图片数据类型有什么区别?为什么需要Tensor数据类型 

两种读取图片的方式

7. 常见的Transforms的使用

Compose 的使用

Python中 __call__ 的用法

ToTensor 的使用

Normalize 的使用

Resize() 的使用

Compose() 的使用

RandomCrop() 的使用

(1)以 int 为例

(2)以 sequence 为例:

总结使用方法

8. torchvision 中的数据集使用

​编辑

CIFAR10数据集

​编辑

​编辑

如何把数据集(多张图片)和 transforms 结合在一起

9. DataLoader 的使用

drop_last

shuffle

10. 神经网络的基本骨架 - nn.Module 的使用

模板:

11. 神经网络 - 卷积层

CIFAR10数据集实例 

卷积前后维度计算公式:

12. 神经网络 - 最大池化的使用

ceil_mode参数

代码实现 

为什么要进行最大池化?最大池化的作用是什么?

用数据集 CIFAR10 实现最大池化

13. 神经网络 - 非线性激活

最常见:RELU

Sigmoid

代码举例:Sigmoid(数据集CIFAR10)

14. 神经网络 - 线性层及其他层介绍

Linear Layers

pytorch提供的一些网络模型

15. 神经网络 - 搭建小实战和 Sequential 的使用

对 CIFAR10 进行分类的简单神经网络

直接搭建,实现上图 CIFAR10 model 的代码

用 Sequential 搭建,实现上图 CIFAR10 model 的代码,并引入 tensorboard 可视化模型结构

16. 损失函数与反向传播

L1LOSS

MSELOSS(均方误差)

CROSSENTROPYLOSS(交叉熵)

在之前写的神经网络中用到 Loss Function(损失函数)

backward  反向传播

17. 优化器

如何使用优化器?

(1)构造

(2)调用优化器的step方法

算法

完整代码

18. 现有网络模型的使用及修改

VGG16 模型

vgg16 网络架构

如何利用现有网络去改动它的结构?

方式一:修改 

方式二:添加

19. 网络模型的保存与读取

两种方式保存模型

方式1:不仅保存了网络模型的结构,也保存了网络模型的参数

方式2:网络模型的参数保存为字典,不保存网络模型的结构(官方推荐的保存方式,用的空间小)

两种方式加载模型

方式1:对应保存方式1,打印出的是网络模型的结构

方式2:对应保存方式2,打印出的是参数的字典形式

20. 完整的模型训练套路

训练过程说明几个点:

完整的代码示例:

model.py

train_cpu.py

train_gpu1.py

train.gpu2.py(用第二张显卡跑)

21. 完整的模型验证(测试、demo)套路

测试过程细节说明:

test.py


 

1.Python学习中的两大法宝函数

pytorch就像一个工具箱,有很多不同的分隔区,每个分隔区里有不同的工具或者更多分割区

探索工具箱,使用两个重要的函数:

  • dir() 函数:能让我们知道工具箱以及工具箱中的分隔区有什么东西(打开,看见)
  • help() 函数能让我们知道每个工具是如何使用的,工具的使用方法(官方说明书)

    例如上图中,输入dir(pytorch),就能看到输出1、2、3、4等分隔区;想继续探索第3个分隔区里有什么的话,输入dir(pytorch.3),输出会是a,b,c(3号分隔区里有a,b,c等工具)输入help(pytorch.3.a)  输出此工具功能:将此扳手放在特定地方,然后拧动

打开Pycharm,在python console中输入:

dir(torch)

 可以看到输出了大量的分隔区(或理解为大量更小的工具箱),输入ctrl+F找到cuda

想看cuda分隔区里有什么,输入

dir(torch.cuda)

可以看到又输出了大量的分隔区

继续输入:

dir(torch.cuda.is_available)

可以看到输出的前后都带有双下划线__,这是一种规范,说明这些变量不容许篡改,即它不再是一个分隔区,说明is_available是一个确确实实的函数(相当于工具,可以用help()函数查看该函数的具体说明)

对工具使用:

help(torch.cuda.is_available)

注意此处 is_available 不带括号

2. Pycharm及Jupyter使用及对比

如何在PyCharm中新建项目?

(1)新建文件夹或者打开现有文件夹

(2)新建 Python文件,命名 first_demo

(3)右键运行程序:

如何在Jupyter中新建项目?

首先在开始菜单打开anaconda的命令行,进入pytorch的conda环境,再打开Jupyter

以每一个为一个运行整体


三种运行方式(PyCharm、PyCharm的Python控制台、Jupyter Notebook)对比:

代码是以块为一个整体运行的,Python文件的块是所有行的代码,即在PyCharm运行时出现错误时,修改后要从头开始运行

PyCharm的Python控制台:一行一行代码运行的,即以每一行为一个块来运行的(也可以以任意行为块运行,按Shift+回车,输入下一行代码,再按回车运行

Jupyter以任意行为块运行

3. PyTorch加载数据初认识

PyTorch 读取数据涉及两个类:Dataset & Dataloader

Dataset:提供一种方式,获取其中需要的数据及其对应的真实的 label 值,并完成编号。

函数主要实现以下两个功能:

  • 怎么去获取每一个数据及其label
  • 告诉我们总共有多少的数据

Dataloader:打包(batch_size),为后面的神经网络提供不同的数据形式

数据集的几种组织形式

上诉train 里有两个文件夹:ants 和 bees,其中分别都是一些蚂蚁和蜜蜂的图片

还有一种标记方式是images是一个文件夹,labels是另一个文件夹,记事本中直接标注图片所属类别.

Dataset类

from torch.utils.data import Dataset

查看Dataset类的介绍:

help(Dataset)

Dataset 是一个抽象类,所有数据集都需要继承这个类,所有子类都需要重写 __getitem__ 的方法,这个方法主要是获取每个数据集及其对应 label,还可以选择性重写长度类 __len__

4. Dataset类代码实战

如何读取图片

# 法1
from PIL import Image
img_path = "xxx"
img = Image.open(img_path)
img.show()
 
# 法2:利用opencv读取图片,获得numpy型图片数据
import cv2  #先在控制台安装pip install opencv-python
cv_img=cv2.imread(img_path)

绝对路径和相对路径:

数据格式一

注意:绝对路径引号前加 r 可以防止转义

完整代码:

from torch.utils.data import Dataset #从torch的常用工具区引入Dataset
from PIL import Image
import os #想要获取所有图片地址(通过索引),需要os库

class MyData(Dataset):  #创建一个MyData的类,需要继承Dataset

    def __init__(self,root_dir,label_dir): #初始化函数---可以理解为C++中的构造函数
        self.root_dir = root_dir #创建时给类的变量可以成为类的全局变量,方便后续的使用
        self.label_dir = label_dir
        self.path =os.path.join(self.root_dir,self.label_dir) # 把两个路径拼接在一起
        self.img_path = os.listdir(self.path)  # 将path路径下所有地址以列表的形式返回

    def __getitem__(self, idx):  # idx为编号
        # 获取每一个图片
        img_name = self.img_path[idx]  # 名称
        img_item_path = os.path.join(self.root_dir, self.label_dir, img_name)  # 获取每张图片的相对路径
        img = Image.open(img_item_path)  # 读取对应idx图片
        label = self.label_dir #图片对应标签
        return img, label

    def __len__(self):  # 数据集的长度
        return len(self.img_path)

# 用类创建实例
root_dir = "dataset/train"
ants_label_dir = "ants"
bees_label_dir = "bees"
ants_dataset = MyData(root_dir, ants_label_dir)
bees_dataset = MyData(root_dir, bees_label_dir)

img, label = ants_dataset[0]
img.show()  # 可视化第一张图片

# 将ants(124张)和bees(121张)两个数据集进行拼接
train_dataset = ants_dataset + bees_dataset

数据格式二

当label比较复杂,存储数据比较多时,不可能以文件夹命名的方式,而是以每张图片对应一个txt文件txt里存储label信息的方式

完整代码:

from torch.utils.data import Dataset
from PIL import Image
import os

class MyData(Dataset):

    def __init__(self,root_dir,img_dir,lable_dir):
        self.root_dir = root_dir
        self.img_dir = img_dir
        self.lable_dir = lable_dir
        self.img_item_path= os.path.join(self.root_dir,self.img_dir)
        self.img_lable_path = os.path.join(self.root_dir, self.lable_dir) #将标签所在的文件夹路径拼接起来
        self.img_list = os.listdir(self.img_item_path)
        self.lable_list = os.listdir(self.img_lable_path)# 将存放标签的文件夹转换成list

    def __getitem__(self, item):
        img_name = self.img_list[item]
        img_name_path = os.path.join(self.img_item_path,img_name)
        img = Image.open(img_name_path)
        lable_name = self.lable_list[item] #对应的标签名字
        lable_name_path = os.path.join(self.img_lable_path,lable_name) #标签的路径
        lable = open(lable_name_path, 'r')  # 打开对应的txt标签文件
        return img,lable

    def __len__(self):
        return len(self.img_list)

root_dir = "dataset2/train"
ants_dir = "ants_image"
lable_dir="ants_label"
ants = MyData(root_dir,ants_dir,lable_dir)
img,lable = ants[0]
img.show()




5. TensorBoard的使用

transforms 在 Dataset 类中很常用,对图像进行变换,如图像要统一到某一个尺寸,或图像中的每一个数据进行类的转换

TensorBoard 探究:

  • 训练过程中loss是如何变化的
  • 模型在不同阶段的输出

SummaryWriter类

from torch.utils.tensorboard import SummaryWriter

查看一个类或方法如何使用:在PyCharm中,按住Ctrl键,把鼠标移到类上,点击进去

是一个直接向 log_dir 文件夹写入的事件文件,可以被 TensorBoard 进行解析

# 实例化SummaryWriter类
writer = SummaryWriter("logs")   # 把对应的事件文件存储到logs文件夹下

主要用到两个方法:

writer.add_image()
writer.add_scalar()
 
writer.close()

直接在 PyCharm 的 Terminal 的 pytorch 环境中安装

输入:

pip install tensorboard

即可安装成功

快捷键注释方法:Ctrl + /  

add_scalar() 方法的使用

def add_scalar(
        self,
        tag,
        scalar_value,
        global_step=None,
        walltime=None,
        new_style=False,
        double_precision=False,
    ):

添加一个标量数据到 Summary 当中,需要参数

  • tag:Data指定方式,类似于图表的title
  • scalar_value:需要保存的数值(y轴)
  • global_step:训练到多少步(x轴)
from torch.utils.tensorboard import SummaryWriter   #导入SummaryWriter类
 
#创建实例
writer=SummaryWriter("logs")   #把对应的事件文件存储到logs文件夹下
 
#两个方法
# writer.add_image()
# y=x
for i in range(100):
    writer.add_scalar("y=x",i,i)
 
writer.close()

运行后多了一个logs文件夹,下面是TensorBoard的一些事件文件:

如何打开事件文件?

在 Terminal 里输入:

tensorboard --logdir=logs  # logdir=事件文件所在文件夹名

结果如图:

为了防止和别人冲突(一台服务器上有好几个人训练,默认打开的都是6006端口),也可以指定端口,命令如下:

​tensorboard --logdir=logs --port=6007

例:y=2x:

from torch.utils.tensorboard import SummaryWriter   #导入SummaryWriter类
 
#创建实例
writer=SummaryWriter("logs")   #把对应的事件文件存储到logs文件夹下
 
#两个方法
# writer.add_image()
# y=2x
for i in range(100):
    writer.add_scalar("y=2x",2*i,i)  # 标题、y轴、x轴
 
writer.close()

title 和 y 不一致的情况:

每向 writer 中写入新的事件,也记录了上一个事件

解决:

把logs文件夹下的所有文件删掉,重新运行代码,在 Terminal 里按 Ctrl+C ,再输入命令:

​tensorboard --logdir=logs 就可以出现名字为y=2x,但实际纵坐标是y=3x数值的图像

add_image() 的使用

  • tag:对应图像的title
  • img_tensor:图像的数据类型,只能是torch.Tensor、numpy.array,string/blobname
  • global_step:训练步骤,int 类型

PIL类型不满足要求:

利用numpy.array(),对PIL图片进行转换

在Python控制台,把PIL类型的img变量转换为numpy类型(add_image() 函数所需要的图像的数据类型),重新赋值给img_array:

import numpy as np
img_array=np.array(img)
print(type(img_array))   # numpy.ndarray

从PIL到numpy,需要在add_image()中dataformats参数中指定shape中每一个数字/维表示的含义 

step1:蚂蚁为例

from torch.utils.tensorboard import SummaryWriter   #导入SummaryWriter类
import numpy as np
from PIL import Image
 
#创建实例
writer=SummaryWriter("logs")   #把对应的事件文件存储到logs文件夹下
image_path="data/train/ants_image/0013035.jpg" #传入相对位置或绝对位置皆可
img_PIL=Image.open(image_path) #打开图片,此时格式为PIL类型
img_array=np.array(img_PIL) #利用numpy进行转换
print(type(img_array))
print(img_array.shape)   #(512,768,3)  即(H,W,C)(高度,宽度,通道)
 
writer.add_image("test",img_array,1, dataformats='HWC')  # 切记需要指定维度
 
writer.close()

结果:

以蜜蜂为例:

from torch.utils.tensorboard import SummaryWriter   #导入SummaryWriter类
import numpy as np
from PIL import Image
 
#创建实例
writer=SummaryWriter("logs")   #把对应的事件文件存储到logs文件夹下
image_path="data/train/bees_image/16838648_415acd9e3f.jpg"
img_PIL=Image.open(image_path)
img_array=np.array(img_PIL)
print(type(img_array))
print(img_array.shape)   #(512,768,3)  即(H,W,C)(高度,宽度,通道)
 
writer.add_image("test",img_array,2, dataformats='HWC')   # 第2步
 
writer.close()

在一个title下,通过滑块显示每一步的图形,可以直观地观察训练中给model提供了哪些数据,或者想对model进行测试时,可以看到每个阶段的输出结果

如果想要单独显示,重命名一下title即可,即 writer.add_image() 的第一个字符串类型的参数

6. torchvision中transforms的使用

transforms的结构及用法

from torchvision import transforms
结构

按住Ctrl,看 transforms.py文件(工具箱),它定义了很多 class文件(工具)

一些类

Compose类:结合不同的transforms

  • ToTensor类:把一个PIL的Image或者numpy数据类型的图片转换成 tensor 的数据类型
  • ToPILImage类:把一个图片转换成PIL Image
  • Normalize类:归一化
  • Resize类尺寸变换
  • CenterCrop类:中心裁剪

1.Transforms该如何使用:

从transforms中选择一个class,对它进行创建,对创建的对象传入图片,即可返回出结果 例如:ToTensor将一个 PIL Image 或 numpy.ndarray 转换为 tensor的数据类型

Ctrl+P可以提示函数里需要填什么参数

# 1、Transforms该如何使用
tensor_trans = transforms.ToTensor()  #从工具箱transforms里取出ToTensor类,返回tensor_trans对象
tensor_img=tensor_trans(img)   #创建出tensor_trans后,传入其需要的参数,即可返回结果
print(tensor_img)

__call__魔术方法,把实例好的对象当作方法(函数),直接加括号就可以调用

魔术方法(也称为双下划线方法)是在对象导向编程中用于定义或重写的特殊方法。在Python中,魔术方法通常以两个下划线开头和结尾,例如__init__()__str__()。它们允许类以特定方式与内置操作交互。例如,__init__()方法用于初始化对象,而__str__()方法被用于返回对象的字符串表示。

可参考博客:理解Python_魔法方法__call___call魔法方法-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/xie_0723/article/details/79505131

2.Tensor数据类型与其他图片数据类型有什么区别?为什么需要Tensor数据类型 

在Python Console输入:

from PIL import Image
from torchvision import transforms
 
img_path= "data/train/ants_image/0013035.jpg"  
img = Image.open(img_path)   
 
tensor_trans = transforms.ToTensor() 
tensor_img = tensor_trans(img)  

打开img,即用Python内置的函数读取的图片,具有的参数有:

再打开tensor_img,看一下它有哪些参数:

为什么需要使用Tensor数据类型:Tensor 数据类型包装了反向神经网络所需要的一些理论基础必须的参数,如:_backward_hooks、_grad等(先转换成Tensor数据类型,再训练)

两种读取图片的方式

1.PIL Image

from PIL import Image
img_path = "xxx"
img = Image.open(img_path)
img.show()

2.numpy.ndarray(通过opencv)

import cv2
cv_img=cv2.imread(img_path)

7. 常见的Transforms的使用

图片有不同的格式,打开方式也不同

Compose 的使用

把不同的 transforms 结合在一起,后面接一个数组,里面是不同的transforms

Example:图片首先要经过中心裁剪,再转换成Tensor数据类型
        >>> transforms.Compose([
        >>>     transforms.CenterCrop(10),
        >>>     transforms.PILToTensor(),
        >>>     transforms.ConvertImageDtype(torch.float),
        >>> ])

Python中 __call__ 的用法

内置函数 __call__ ,不用.的方式调用方法,可以直接拿对象名,加上需要的参数,即可调用方法

class Person:
    def __call__(self, name):   #下划线__表示为内置函数
        print("__call__"+"Hello "+name)
 
    def hello(self,name):
        print("hello"+name)
 
person = Person()
person("zhangsan")
person.hello("lisi")

输出结果如下:

__call__Hello zhangsan
hellolisi

ToTensor 的使用

把 PIL Image 或 numpy.ndarray 类型转换为 tensor 类型(TensorBoard 必须是 tensor 的数据类型

from PIL import Image
from torchvision import transforms
from torch.utils.tensorboard import SummaryWriter

image_path = r"F:\learn_pytorch\pythonProject\image\bbf69b4c9b3437cf78986a9c8b8fe315.jpg"
image = Image.open(image_path)

img_trans = transforms.ToTensor()
img_tensor = img_trans(image)
print(img_tensor)

writer = SummaryWriter("log")
writer.add_image("tensor",img_tensor,1) #图片类型默认是CHW,故不需要显式设置参数dataformats

(运行前要先把之前的logs进行删除)运行后,在 Terminal 里输入:

tensorboard --logdir=logs

进入网站后可以看到图片:


Normalize 的使用

图片RGB三个信道,将每个信道中的输入进行标准化

output[channel] = (input[channel] - mean[channel]) / std[channel]
设置 mean 和 std 都为0.5,则 output= 2*input -1。如果 input 图片像素值为0~1范围内,那么结果就是 -1~1之间

img_norm = transforms.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])
img_n = img_norm(img_tensor)
print(img_n)

writer = SummaryWriter("log")
writer.add_image("tensor",img_n,2)
writer.close()

刷新网页:

更改mean和std值后刷新网页:

Resize() 的使用

输入:PIL或者Tensor    将输入转变到给定尺寸

  • 序列:(h,w)高度,宽度
  • 一个整数:不改变高和宽的比例,只单纯改变最小边和最长边之间的大小关系。之前图里最小的边将会匹配这个数(等比缩放)

PyCharm小技巧设置:忽略大小写,进行提示匹配

一般情况下,你需要输入R,才能提示出Resize
我们想设置,即便你输入的是r,也能提示出Resize,也就是忽略了大小写进行匹配提示
File—> Settings—> 搜索case—> Editor-General-Code Completion-去掉Match case前的√

—>Apply—>OK

#Resize
# Resize的使用
print(image.size)  # 输入是PIL.Image

trans_resize = transforms.Resize((512, 512))
# img:PIL --> resize --> img_resize:PIL   输入是PIL图片,输出还是PIL图片
img_resize = trans_resize(image)  # 输出还是PIL Image

# img_resize:PIL --> totensor --> img_resize:tensor
img_resize = img_trans(img_resize)

writer = SummaryWriter("file")
writer.add_image("Resize", img_resize, 0)
print(img_resize)

Compose() 的使用

Compose() 中的参数需要是一个列表,Python中列表的表示形式为[数据1,数据2,...]

在Compose中,数据需要是transforms类型,所以得到Compose([transforms参数1,transforms参数2,...])

#Compose的使用(将输出类型从PIL变为tensor类型,第二种方法)
 
trans_resize_2 = transforms.Resize(512)  # 将图片短边缩放至512,长宽比保持不变
 
# PIL --> resize --> PIL --> totensor --> tensor
#compose()就是把两个参数功能整合,第一个参数是改变图像大小,第二个参数是转换类型,前者的输出类型与后者的输入类型必须匹配
 
trans_compose = transforms.Compose([trans_resize_2,trans_totensor])
img_resize_2 = trans_compose(img)   # 输入需要是PIL Image
writer.add_image("Resize",img_resize_2,1)

RandomCrop() 的使用

参数size:

  • sequence:(h,w) 高,宽
  • int:裁剪一个该整数×该整数的图像

(1)以 int 为例

#RandomCrop()的使用
trans_random = transforms.RandomCrop(512)
trans_compose_2 = transforms.Compose([trans_random,trans_totensor])
writer = SummaryWriter("file1")
for i in range(10):  #裁剪10个
    img_crop = trans_compose_2(image)  # 输入需要是PIL Image
    writer.add_image("RandomCrop",img_crop,i)

输出结果:

(2)以 sequence 为例:

#RandomCrop()的使用
trans_random = transforms.RandomCrop((500,1000))
trans_compose_2 = transforms.Compose([trans_random,trans_totensor])
writer = SummaryWriter("file1")
for i in range(10):  #裁剪10个
    img_crop = trans_compose_2(image)
    writer.add_image("RandomCropHW",img_crop,i)

输出结果:

总结使用方法


1.关注transforms中参数输入和输出类型
2.多看官方文档
3.关注方法需要什么参数:参数如果设置了默认值,保留默认值即可,没有默认值的需要指定(看一下要求传入什么类型的参数)
4.不知道变量的输出类型可以:
直接print该变量,print(type()),看结果里显示什么类型
最后要 totensor,在 tensorboard 看一下结果(tensorboard需要tensor数据类型进行显示)

8. torchvision 中的数据集使用

CIFAR10数据集

数据集下载过慢时:

获得下载链接后,把下载链接放到迅雷中,会首先下载压缩文件tar.gz,之后会对该压缩文件进行解压,里面会有相应的数据集

采用迅雷下载完毕后,在PyCharm里新建directory,名字也叫dataset,再将下载好的压缩包复制进去,download依然为True,运行后,会自动解压该数据

没有显示下载地址时:

按住Ctrl键,查看数据集的源代码,若其中有 url地址,可直接复制到迅雷中进行下载

如何把数据集(多张图片)和 transforms 结合在一起


CIFAR10数据集原始图片是PIL Image,如果要给pytorch使用,需要转为tensor数据类型(转成tensor后,就可以用tensorboard了)

transforms 更多地是用在 datasets 里 transform 的选项中

import torchvision
from torch.utils.tensorboard import SummaryWriter

# 把dataset_transform运用到数据集中的每一张图片,都转为tensor数据类型
dataset_transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor()
])

train_set = torchvision.datasets.CIFAR10(root="./dataset", train=True, transform=dataset_transform,
                                         download=True)  # root使用相对路径,会在该.py所在位置创建一个叫dataset的文件夹,同时把数据保存进去
test_set = torchvision.datasets.CIFAR10(root="./dataset", train=False, transform=dataset_transform, download=True)

# print(test_set[0])

writer = SummaryWriter("p10")
# 显示测试数据集中的前10张图片
for i in range(10):
    img, target = test_set[i]
    writer.add_image("test_set", img, i)  # img已经转成了tensor类型

writer.close()

运行结果:

9. DataLoader 的使用

  • dataset:告诉程序中数据集的位置,数据集中索引,数据集中有多少数据(想象成一叠扑克牌)
  • dataloader:将数据加载到神经网络中,每次从dataset中取数据,通过dataloader中的参数可以设置如何取数据(想象成抓的一组牌)

参数:

batch_size:每次抓牌抓几张
shuffle:打乱与否,值为True的话两次打牌时牌的顺序是不一样。默认为False,但一般用True
num_workers:加载数据时采用单个进程还是多个进程,多进程的话速度相对较快,默认为0(主进程加载)。Windows系统下该值>0会有问题(报错提示:BrokenPipeError)
drop_last:100张牌每次取3张,最后会余下1张,这时剩下的这张牌是舍去还是不舍去。值为True代表舍去这张牌、不取出,False代表要取出该张牌

示例:

dataset 

   __getitem()__:return img,target
dataloader(batch_size=4):从dataset中取4个数据

img0,target0 = dataset[0]
img1,target1 = dataset[1]
img2,target2 = dataset[2]
img3,target3 = dataset[3]

把 img 0-3 进行打包,记为imgs;target 0-3 进行打包,记为targets;作为dataloader中的返回

for data in test_loader:
    imgs,targets = data
    print(imgs.shape)
    print(targets)

输出结果:

torch.Size([4, 3, 32, 32])   #4张图片,三通道,32×32
tensor([0, 4, 4, 8])  #4个target进行一个打包

数据是随机取的(断点debug一下,可以看到采样器sampler是随机采样的),所以两次的 target 0 并不一样

# 用上节课torchvision提供的自定义的数据集
# CIFAR10原本是PIL Image,需要转换成tensor
 
import torchvision.datasets
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
 
# 准备的测试数据集
test_data = torchvision.datasets.CIFAR10("./dataset",train=False,transform=torchvision.transforms.ToTensor())
 
# 加载测试集
test_loader = DataLoader(dataset=test_data,batch_size=64,shuffle=True,num_workers=0,drop_last=False)
#batch_size=4,意味着每次从test_data中取4个数据进行打包
 
writer = SummaryWriter("dataloader")
step=0
for data in test_loader:
    imgs,targets = data  #imgs是tensor数据类型
    writer.add_images("test_data",imgs,step)
    step=step+1
 
writer.close()

运行结果如图,滑动滑块即是每一次取数据时的batch_size张图片:

由于 drop_last 设置为 False,所以最后16张图片(没有凑齐64张)显示如下:

drop_last

若将 drop_last 设置为 True,最后16张图片(step 156)会被舍去,结果如图:

shuffle


一个 for data in test_loader 循环,就意味着打完一轮牌(抓完一轮数据),在下一轮再进行抓取时,第二次数据是否与第一次数据一样。值为True的话,会重新洗牌(一般都设置为True)

shuffle为False的话两轮取的图片是一样的

在外面再套一层 for epoch in range(2) 的循环

结果如下:

可以看出两次 step 155 的图片不一样

10. 神经网络的基本骨架 - nn.Module 的使用

其中最常用的是 Module 模块(为所有神经网络提供基本骨架)

模板:

import torch.nn as nn
import torch.nn.functional as F
 
class Model(nn.Module):   #搭建的神经网络 Model继承了 Module类(父类)
    def __init__(self):   #初始化函数
        super(Model, self).__init__()   #必须要这一步,调用父类的初始化函数
        self.conv1 = nn.Conv2d(1, 20, 5)
        self.conv2 = nn.Conv2d(20, 20, 5)
 
    def forward(self, x):   #前向传播(为输入和输出中间的处理过程),x为输入
        x = F.relu(self.conv1(x))   #conv为卷积,relu为非线性处理
        return F.relu(self.conv2(x))

前向传播 forward(在所有子类中进行重写

11. 神经网络 - 卷积层

图像为二维矩阵,常使用二维卷积

参数:

动图:

蓝色的为输入图像,绿色的为输出图像,黑色阴影部分为卷积核

kernel_size:
定义了一个卷积核的大小,若为3则生成一个3×3的卷积核

卷积核的参数是从一些分布中进行采样得到的,实际训练过程中,卷积核中的值会不断进行调整
in_channels & out_channels
in_channels:输入图片的channel数(彩色图像 in_channels 值为3)
out_channels:输出图片的channel数
in_channels 和 out_channels 都为 1 时,拿一个卷积核在输入图像中进行卷积

out_channels 为 2 时,卷积层会生成两个卷积核(两个卷积核不一定一样),得到两个输出

CIFAR10数据集实例 

# CIFAR10数据集
import torch
import torchvision
from torch import nn
from torch.nn import Conv2d
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)  # 这里用测试数据集,因为训练数据集太大了
dataloader = DataLoader(dataset, batch_size=64)


# 搭建神经网络Tudui
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        # 因为是彩色图片,所以in_channels=3
        self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0)  # 卷积层conv1

    def forward(self, x):  # 输出为x
        x = self.conv1(x)
        return x


tudui = Tudui()  # 初始化网络
# 打印一下网络结构
print(tudui)  # Tudui((conv1): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1)))

writer = SummaryWriter("conv")
step = 0
for data in dataloader:
    imgs, targets = data  # 经过ToTensor转换,成为tensor数据类型,可以直接送到网络中
    output = tudui(imgs)
    print(imgs.shape)  # 输入大小 torch.Size([64, 3, 32, 32])  batch_size=64,in_channels=3(彩色图像),每张图片是32×32的
    print(output.shape)  # 经过卷积后的输出大小 torch.Size([64, 6, 30, 30])  卷积后变成6个channels,但原始图像减小,所以是30×30的
    writer.add_images("input", imgs, step)

    # 6个channel无法显示出来,所以需要经过转换reshape
    # torch.Size([64, 6, 30, 30]) ——> [xxx,3,30,30] 第一个值不知道为多少时写-1,会根据后面值的大小进行计算
    output = torch.reshape(output, (-1, 3, 30, 30))
    writer.add_images("output", output, step)
    step = step + 1

writer.close()

运行后的结果:

卷积前后维度计算公式:

很多论文中padding和stride为给出,需要通过公式去进行计算

比如VGG16模型:第一层的卷积进行了padding填充是经过公式计算得出

12. 神经网络 - 最大池化的使用

torch.nn — PyTorch 2.4 documentation

  • MaxPool:最大池化(下采样)(最常用)
  • MaxUnpool:上采样
  • AvgPool:平均池化
  • AdaptiveMaxPool2d:自适应最大池化

参数:

注意,卷积中stride默认为1,而池化中stride默认为kernel_size

注意:黑色阴影部分为卷积核

ceil_mode参数

Ceil_mode 默认情况下为 False,对于最大池化一般只需设置 kernel_size 即可

代码实现 

要求的 input 必须是四维的,参数依次是:batch_size、channel、高、宽 

上述图用代码实现:(以 Ceil_mode = True 为例)

import torch
from torch import nn
from torch.nn import MaxPool2d
 
input = torch.tensor([[1,2,0,3,1],
                      [0,1,2,3,1],
                      [1,2,1,0,0],
                      [5,2,3,1,1],
                      [2,1,0,1,1]],dtype=torch.float32)  #最大池化无法对long数据类型进行实现,将input变成浮点数的tensor数据类型
input = torch.reshape(input,(-1,1,5,5))  #-1表示torch计算batch_size
print(input.shape)
 
# 搭建神经网络
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.maxpool1 = MaxPool2d(kernel_size=3,ceil_mode=True)
    def forward(self,input):
        output = self.maxpool1(input)
        return output
 
# 创建神经网络
tudui = Tudui()
output = tudui(input)
print(output)

运行结果:

为什么要进行最大池化?最大池化的作用是什么?


最大池化的目的是保留输入的特征,同时把数据量减小(数据维度变小),对于整个网络来说,进行计算的参数变少,会训练地更快

如上面案例中输入是5x5的,但输出是3x3的,甚至可以是1x1的
类比:1080p的视频为输入图像,经过池化可以得到720p,也能满足绝大多数需求,传达视频内容的同时,文件尺寸会大大缩小

用数据集 CIFAR10 实现最大池化

import torch
import torchvision
from torch import nn
from torch.nn import MaxPool2d
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("./dataset", train=False, download=True,
                                       transform=torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, batch_size=64)


# 搭建神经网络
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.maxpool1 = MaxPool2d(kernel_size=3, ceil_mode=True)

    def forward(self, input):
        output = self.maxpool1(input)
        return output


# 创建神经网络
tudui = Tudui()

writer = SummaryWriter("maxpool")
step = 0
for data in dataloader:
    imgs, targets = data
    writer.add_images("input", imgs, step)
    output = tudui(imgs)
    # output尺寸池化后不会有多个channel,原来是3维的图片,经过最大池化后还是3维的,不需要像卷积一样还要reshape操作(影响通道数的是卷积核个数)
    writer.add_images("output", output, step)
    step = step + 1

writer.close()

运行结果:(经过池化操作后,可以发现图片清晰度降低,像是打了马赛克一样的效果,但是图片的细节信息一样还是可以保持)

13. 神经网络 - 非线性激活

非线性激活:给神经网络引入一些非线性的特征

非线性越多,才能训练出符合各种曲线或特征的模型(提高泛化能力)

最常见:RELU

(输入:(N,*)    N 为 batch_size,*不限制)

代码举例:

import torch
from torch import nn
from torch.nn import ReLU
 
input = torch.tensor([[1,-0.5],
                      [-1,3]])
input = torch.reshape(input,(-1,1,2,2))  #input必须要指定batch_size,-1表示batch_size自己算,1表示是1维的
print(input.shape)   #torch.Size([1, 1, 2, 2])
 
# 搭建神经网络
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.relu1 = ReLU()  #inplace默认为False
    def forward(self,input):
        output = self.relu1(input)
        return output
 
# 创建网络
tudui = Tudui()
output = tudui(input)
print(output)

运行结果:

关于inplace:

Sigmoid

 输入:(N,*)    N 为 batch_size,*不限制

代码举例:Sigmoid(数据集CIFAR10)
import torch
import torchvision.datasets
from torch import nn
from torch.nn import Sigmoid
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

trans = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor()
])
dataset = torchvision.datasets.CIFAR10("./dataset", train=False, download=True,
                                       transform=trans)
dataloader = DataLoader(dataset, batch_size=64)


# 搭建神经网络
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.sigmoid1 = Sigmoid()  # inplace默认为False

    def forward(self, input):
        output = self.sigmoid1(input)
        return output


# 创建网络
tudui = Tudui()

writer = SummaryWriter("sigmoid")
step = 0
for data in dataloader:
    imgs, targets = data
    writer.add_images("input", imgs, global_step=step)
    output = tudui(imgs)
    writer.add_images("output", output, step)
    step = step + 1

writer.close()

运行结果:

14. 神经网络 - 线性层及其他层介绍

Linear Layers

import torch
import torchvision.datasets
from torch import nn
from torch.nn import Linear
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=64, drop_last=True)


class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.linear1 = Linear(196608, 10)

    def forward(self, input):
        output = self.linear1(input)
        return output


tudui = Tudui()

for data in dataloader:
    imgs, targets = data
    print(imgs.shape)  # torch.Size([64, 3, 32, 32])
    # output = torch.reshape(imgs,(1,1,1,-1))  # reshape按照指定维度把图片展平
    # print(output.shape)  # torch.Size([1, 1, 1, 196608])
    # output = tudui(output)
    # print(output.shape)  # torch.Size([1, 1, 1, 10])
    output = torch.flatten(imgs)  # flatten摊平成一行
    print(output.shape)  # torch.Size([196608])
    output = tudui(output)
    print(output.shape)  # torch.Size([10])

运行结果:

pytorch提供的一些网络模型

15. 神经网络 - 搭建小实战和 Sequential 的使用

好处:代码简洁易懂

对 CIFAR10 进行分类的简单神经网络

CIFAR 10:根据图片内容,识别其究竟属于哪一类(10代表有10个类别)

第一次卷积:首先加了几圈 padding(图像大小不变,还是32x32),然后卷积了32次

这里是假定stride=1,然后再求出padding.如何假定stride=2,padding过大,显然不合理.

直接搭建,实现上图 CIFAR10 model 的代码

from torch import nn
from torch.nn import Conv2d,MaxPool2d,Flatten,Linear
import torch


class yyh(nn.Module):
    def __init__(self):
        super(yyh, self).__init__()
        self.conv1 = Conv2d(3,32,5,stride=1,padding=2)
        self.maxpool1 = MaxPool2d(2)
        self.conv2 = Conv2d(32,32,5,stride=1,padding=2)#维持尺寸不变,所以padding仍为2
        self.maxpool2 = MaxPool2d(2)
        self.conv3 = Conv2d(32,64,5,stride=1,padding=2)
        self.maxpool3 = MaxPool2d(2)
        self.flatten = Flatten()
        #经过两个线性层:第一个线性层(1024为in_features,64为out_features)
        # 第二个线性层(64为in_features,10为out_features)
        self.Linear1 = Linear(1024,64)
        self.Linear2 = Linear(64,10)

    def forward(self,input):
        input = self.conv1(input)
        input = self.maxpool1(input)
        input = self.conv2(input)
        input = self.maxpool2(input)
        input = self.conv3(input)
        input = self.maxpool3(input)
        input = self.flatten(input)
        input = self.Linear1(input)
        input = self.Linear2(input)
        return input

Yyh = yyh()
print(Yyh)

input = torch.ones((64,3,32,32))##全是1,batch_size=64(64张图片),3通道,32x32
output = Yyh(input)
print(output.shape)

用 Sequential 搭建,实现上图 CIFAR10 model 的代码,并引入 tensorboard 可视化模型结构

作用:代码更加简洁

from torch import nn
from torch.nn import Conv2d,MaxPool2d,Flatten,Linear,Sequential
import torch
from torch.utils.tensorboard import SummaryWriter

class yyh(nn.Module):
    def __init__(self):
        super(yyh, self).__init__()
        self.model = Sequential(
            Conv2d(3, 32, 5, stride=1, padding=2),
            MaxPool2d(2),
            Conv2d(32, 32, 5, stride=1, padding=2),
            MaxPool2d(2),
            Conv2d(32, 64, 5, stride=1, padding=2),
            MaxPool2d(2),
            Flatten(),
            Linear(1024, 64),
            Linear(64, 10)
        )

    def forward(self,input):
        input = self.model(input)
        return input

Yyh = yyh()
print(Yyh)

input = torch.ones((64,3,32,32))##全是1,batch_size=64(64张图片),3通道,32x32
output = Yyh(input)
print(output.shape)

writer = SummaryWriter('seqs')
writer.add_graph(Yyh,input)
writer.close()

运行结果:

可视化结果:

16. 损失函数与反向传播

torch.nn 里的 loss function 衡量误差,在使用过程中根据需求使用,注意输入形状和输出形状即可loss 衡量实际神经网络输出 output 与真实想要结果 target 的差距,越小越好

作用:

计算实际输出和目标之间的差距
为我们更新输出提供一定的依据(反向传播):给每一个卷积核中的参数提供了梯度 grad,采用反向传播时,每一个要更新的参数都会计算出对应的梯度,优化过程中根据梯度对参数进行优化,最终达到整个 loss 进行降低的目的

L1LOSS

torch.nn.L1Loss(size_average=None, reduce=None, reduction='mean')

input:(N,*)    N是batch_size,即有多少个数据;*可以是任意维度 

取平均的代码:

import torch
from torch.nn import L1Loss
 
# 实际数据或网络默认情况下就是float类型,不写测试案例的话一般不需要加dtype
inputs = torch.tensor([1,2,3],dtype=torch.float32)   # 计算时要求数据类型为浮点数,不能是整型的long
targets = torch.tensor([1,2,5],dtype=torch.float32)
 
inputs = torch.reshape(inputs,(1,1,1,3))   # 1 batch_size, 1 channel, 1行3列
targets = torch.reshape(targets,(1,1,1,3))
 
loss = L1Loss()
result = loss(inputs,targets)
print(result)

运行结果:

求和的代码:(只需要修改其中一处代码)

loss = L1Loss(reduction='sum')

运行结果:

MSELOSS(均方误差)

input:(N,*)    N是batch_size,即有多少个数据;*可以是任意维度 

求和的代码:

求平均的代码:

手动计算示例:

CROSSENTROPYLOSS(交叉熵)

适用于训练分类问题,有C个类别

input 为没有进行处理过的对每一类的得分

在之前写的神经网络中用到 Loss Function(损失函数)

import torchvision.datasets
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=1)


class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model1 = Sequential(
            Conv2d(3, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 64, 5, padding=2),
            MaxPool2d(2),
            Flatten(),
            Linear(1024, 64),
            Linear(64, 10)
        )

    def forward(self, x):  # x为input
        x = self.model1(x)
        return x


loss = nn.CrossEntropyLoss()

tudui = Tudui()

for data in dataloader:
    imgs, targets = data  # imgs为输入,放入神经网络中
    outputs = tudui(imgs)  # outputs为输入通过神经网络得到的输出,targets为实际输出
    result_loss = loss(outputs, targets)
    print(result_loss)  # 神经网络输出与真实输出的误差

结果:

backward  反向传播

计算出每一个节点参数的梯度

在上述代码后加一行:

import torchvision.datasets
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=1)


class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model1 = Sequential(
            Conv2d(3, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 64, 5, padding=2),
            MaxPool2d(2),
            Flatten(),
            Linear(1024, 64),
            Linear(64, 10)
        )

    def forward(self, x):  # x为input
        x = self.model1(x)
        return x


loss = nn.CrossEntropyLoss()

tudui = Tudui()

for data in dataloader:
    imgs, targets = data  # imgs为输入,放入神经网络中
    outputs = tudui(imgs)  # outputs为输入通过神经网络得到的输出,targets为实际输出
    result_loss = loss(outputs, targets)
    print(result_loss)  # 神经网络输出与真实输出的误差
    result_loss.backward()  # backward反向传播,是对result_loss,而不是对loss

在加上的这一句代码前打上断点(运行到该行代码的前一行,该行不运行),debug 后:

 tudui ——> model 1 ——> Protected Attributes ——> _modules ——> '0' ——> bias / weight——> grad(是None)

点击Step into My Code,运行完该行后,可以发现刚刚的None有值了(损失函数一定要经过 .backward() 后才能反向传播,才能有每个需要调节的参数的grad的值

17. 优化器

当使用损失函数时,可以调用损失函数的 backward,得到反向传播,反向传播可以求出每个需要调节的参数对应的梯度,有了梯度就可以利用优化器优化器根据梯度对参数进行调整,以达到整体误差降低的目的

torch.optim — PyTorch 2.4 documentationicon-default.png?t=O83Ahttps://pytorch.org/docs/stable/optim.html

如何使用优化器?

(1)构造

(2)调用优化器的step方法

利用之前得到的梯度对参数进行更新

for input, target in dataset:
    optimizer.zero_grad() #把上一步训练的每个参数的梯度清零
    output = model(input)
    loss = loss_fn(output, target)  # 输出跟真实的target计算loss
    loss.backward() #调用反向传播得到每个要更新参数的梯度
    optimizer.step() #每个参数根据上一步得到的梯度进行优化

算法

如Adadelta、Adagrad、Adam、RMSProp、SGD等等,不同算法前两个参数params、lr 都是一致的,后面的参数不同

学习速率不能太大(太大模型训练不稳定)也不能太小(太小模型训练慢),一般建议先采用较大学习速率,后采用较小学习速率

完整代码

在 data 循环外又套一层 epoch 循环,一次 data 循环相当于对数据训练一次,加了 epoch 循环相当于对数据训练 20 次

import torch
import torchvision.datasets
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.data import DataLoader

# 加载数据集并转为tensor数据类型
dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=1)


# 创建网络名叫Tudui
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model1 = Sequential(
            Conv2d(3, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 64, 5, padding=2),
            MaxPool2d(2),
            Flatten(),
            Linear(1024, 64),
            Linear(64, 10)
        )

    def forward(self, x):  # x为input,forward前向传播
        x = self.model1(x)
        return x


# 计算loss
loss = nn.CrossEntropyLoss()

# 搭建网络
tudui = Tudui()

# 设置优化器
optim = torch.optim.SGD(tudui.parameters(), lr=0.01)  # SGD随机梯度下降法
for epoch in range(20):
    running_loss = 0.0  # 在每一轮开始前将loss设置为0
    for data in dataloader:  # 该循环相当于只对数据进行了一轮学习
        imgs, targets = data  # imgs为输入,放入神经网络中
        outputs = tudui(imgs)  # outputs为输入通过神经网络得到的输出,targets为实际输出
        result_loss = loss(outputs, targets)
        optim.zero_grad()  # 把网络模型中每一个可以调节的参数对应梯度设置为0
        result_loss.backward()  # backward反向传播求出每一个节点的梯度,是对result_loss,而不是对loss
        optim.step()  # 对每个参数进行调优
        running_loss = running_loss + result_loss  # 每一轮所有loss的和
    print(running_loss)

部分运行结果:

优化器对模型参数不断进行优化,每一轮的 loss 在不断减小

实际过程中模型在整个数据集上的训练次数(即最外层的循环)都是成百上千/万的,本例仅以 20 次举例

18. 现有网络模型的使用及修改

  • 本节讲述:
  • 如何加载现有的一些 pytorch 提供的网络模型
  • 如何对网络模型中的结构进行修改,包括添加自己想要的一些网络模型结构

本节主要以Classification 里的 VGG 模型为例,数据集仍为 CIFAR10 数据集(主要用于分类)

注意:

./xxx表示当前路径下,../xxx表示返回上一级目录 

多行注释快捷键:ctrl+/

VGG16 模型

  • weights为 None 的情况下,只是加载网络模型,参数都为默认参数,不需要下载
  • 为VGG16_Weights时需要从网络中下载,卷积层、池化层对应的参数等等(在ImageNet数据集中训练好的)

vgg16 网络架构

import torchvision.models
from torchvision.models import VGG16_Weights

vgg16_false = torchvision.models.vgg16(weights=None)  # 另一个参数progress显示进度条,默认为True
vgg16_true = torchvision.models.vgg16(weights=VGG16_Weights.IMAGENET1K_V1)

print(vgg16_true)

输出:

VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU(inplace=True)
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU(inplace=True)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU(inplace=True)
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU(inplace=True)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU(inplace=True)
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU(inplace=True)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU(inplace=True)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU(inplace=True)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU(inplace=True)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU(inplace=True)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU(inplace=True)
    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace=True)
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

Process finished with exit code 0

如何利用现有网络去改动它的结构?

CIFAR10 把数据分成了10类,而 vgg16 模型把数据分成了 1000 类,如何应用这个网络模型呢?

  • 方式一:把最后线性层的 out_features 从1000改为10
  • 方式二:在最后的线性层下面再加一层,in_features为1000,out_features为10

利用现有网络去改动它的结构,避免写 vgg16

很多框架会把 vgg16 当做前置的网络结构,提取一些特殊的特征,再在后面加一些网络结构,实现功能。

方式一:修改 

以 vgg16_false 为例:

想将最后一层 Linear 的 out_features 改为10:

import torchvision.models
from torch import nn
from torchvision.models import VGG16_Weights

vgg16_false = torchvision.models.vgg16(weights=None)  # 另一个参数progress显示进度条,默认为True
vgg16_true = torchvision.models.vgg16(weights=VGG16_Weights.IMAGENET1K_V1)

print(vgg16_false)

vgg16_false.classifier[6] = nn.Linear(4096,10)
print(vgg16_false)

结果如下:

方式二:添加

以 vgg16_true 为例:

# 给 vgg16 添加一个线性层,输入1000个类别,输出10个类别
vgg16_true.add_module('add_linear',nn.Linear(in_features=1000,out_features=10))
print(vgg16_true)

结果如图:

如果想将 module 添加至 classifier 里:

# 给 vgg16 添加一个线性层,输入1000个类别,输出10个类别
vgg16_true.classifier.add_module('7',nn.Linear(in_features=1000,out_features=10))
print(vgg16_true)

结果如图:

19. 网络模型的保存与读取

两种方式保存模型

方式1:不仅保存了网络模型的结构,也保存了网络模型的参数

import torch
import torchvision.models
from torch import nn

vgg16 = torchvision.models.vgg16()

print(vgg16)

# 保存方式1:模型结构+模型参数
torch.save(vgg16,"vgg16_method1.pth")

运行结果:

方式2:网络模型的参数保存为字典,不保存网络模型的结构(官方推荐的保存方式,用的空间小)

import torch
import torchvision.models
from torch import nn

vgg16 = torchvision.models.vgg16()

print(vgg16)

# 保存方式2:模型参数(官方推荐)
torch.save(vgg16.state_dict(),"vgg16_method2.pth")
# 把vgg16的状态保存为字典形式(Python中的一种数据格式)

运行结果:

两种方式加载模型

方式1:对应保存方式1,打印出的是网络模型的结构

import torch
import torchvision.models
from torch import nn

vgg16 = torchvision.models.vgg16()

print(vgg16)

torch.save(vgg16,"vgg16_method1.pth")

# 方式1 对应 保存方式1,加载模型
model = torch.load("vgg16_method1.pth",)
print(model)  # 打印出的只是模型的结构,其实它的参数也被保存下来了

运行结果:

方式2:对应保存方式2,打印出的是参数的字典形式

import torch
import torchvision.models
from torch import nn

vgg16 = torchvision.models.vgg16()

torch.save(vgg16.state_dict(),"vgg16_method2.pth")

# 方式2 加载模型
model = torch.load("vgg16_method2.pth")
print(model)  # 打印出的只是模型的结构,其实它的参数也被保存下来了

运行结果:

如何恢复网络模型结构?

import torch
import torchvision.models
from torch import nn

vgg16 = torchvision.models.vgg16()

torch.save(vgg16.state_dict(),"vgg16_method2.pth")

vgg16.load_state_dict(torch.load("vgg16_method2.pth"))
print(vgg16)

20. 完整的模型训练套路

在语句后面按 Ctrl + d 可以复制这条语句

训练过程说明几个点:

1.测试过程中不需要对模型进行调优,利用现有模型进行测试,所以有以下命令:

with torch.no_grad(): 

2.为了使测试的 loss 结果易找,在 train.py 中添加了一句 if 的代码,使train每训练100轮才打印1次:

if total_train_step % 100 ==0:  # 逢百才打印记录
    print("训练次数:{},loss:{}".format(total_train_step,loss.item()))

3.实际写项目过程中,模型直接定义在一个单独的文件中(如model.py),再在 训练文件中导入:
 

from model import *

4.与tensorboard结合可视化训练结果:

writer.add_scalar("train_loss",loss.item(),total_train_step)
writer.add_scalar("test_loss", total_test_loss, total_test_step)

5.保存每一轮的训练结果:

torch.save(tudui, "tudui_{}.pth".format(i))  # 每一轮保存一个结果
    print("模型已保存")

6.即便得到整体测试集上的 loss,也不能很好说明在测试集上的表现效果,在分类问题中可以用正确率表示:

accuracy = (outputs.argmax(1) == targets).sum()
#argmax取出指定方向的最大值
# 1代表:横向比较。==(逻辑比较):True或False,sum:计算True或False个数
total_accuracy = total_accuracy + accuracy
#计算总的精确个数

7.model.train() 和 model.eval()的作用

训练步骤开始之前会把网络模型(我们这里的网络模型叫 tudui)设置为train,并不是说把网络设置为训练模型它才能够开始训练

测试网络前写 网络.eval(),并不是说需要这一行才能把网络设置成 eval 状态,才能进行网络测试

作用:

这两句不写网络依然可以运行,它们的作用是:

本节写的案例没有 Dropout 层或 BatchNorm 层,所以有没有这两行无所谓

如果有这些特殊层,一定要调用

8.利用GPU训练:

第一种方式:(不常用)

第二种方式:

9.比较CPU/GPU训练时间

为了比较时间,引入 time 这个 package

import time 

完整的代码示例:

model.py

import torch
from torch import nn


# 搭建神经网络(10分类网络)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        # 把网络放到序列中
        self.model = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
            # 输入是32x32的,输出还是32x32的(padding经计算为2)
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
            # 输入输出都是16x16的(同理padding经计算为2)
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
            nn.MaxPool2d(kernel_size=2),
            nn.Flatten(),  # 展平
            nn.Linear(in_features=64 * 4 * 4, out_features=64),
            nn.Linear(in_features=64, out_features=10)
        )

    def forward(self, x):
        x = self.model(x)
        return x


if __name__ == '__main__':#为了让这段代码在此脚本文件执行时才运行
    # 测试网络的验证正确性
    tudui = Tudui()
    input = torch.ones((64, 3, 32, 32))  # batch_size=64(代表64张图片),3通道,32x32
    output = tudui(input)
    print(output.shape)

train_cpu.py

import torchvision.datasets
from torch.utils.tensorboard import SummaryWriter

from model import *
from torch import nn
from torch.utils.data import DataLoader
import time
# 准备数据集,CIFAR10 数据集是PIL Image,要转换为tensor数据类型
train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=True)

# 看一下训练数据集和测试数据集都有多少张(如何获得数据集的长度)
train_data_size = len(train_data)  # length 长度
test_data_size = len(test_data)
# 如果train_data_size=10,那么打印出的字符串为:训练数据集的长度为:10
print("训练数据集的长度为:{}".format(train_data_size))  # 字符串格式化,把format中的变量替换{}
print("测试数据集的长度为:{}".format(test_data_size))

# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 创建网络模型
tudui = Tudui()

# 创建损失函数
loss_fn = nn.CrossEntropyLoss()  # 分类问题可以用交叉熵

# 定义优化器
learning_rate = 0.01  # 另一写法:1e-2,即1x 10^(-2)=0.01
optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate)  # SGD 随机梯度下降

# 设置训练网络的一些参数
total_train_step = 0  # 记录训练次数
total_test_step = 0  # 记录测试次数
epoch = 10  # 训练轮数

# 添加tensorboard
writer = SummaryWriter("../logs_train")

start_time = time.time()
for i in range(epoch):
    print("----------第{}轮训练开始-----------".format(i + 1))  # i从0-9
    # 训练步骤开始
    tudui.train()
    for data in train_dataloader:
        imgs, targets = data
        outputs = tudui(imgs)
        loss = loss_fn(outputs, targets)

        # 优化器优化模型
        optimizer.zero_grad()  # 首先要梯度清零
        loss.backward()  # 反向传播得到每一个参数节点的梯度
        optimizer.step()  # 对参数进行优化
        total_train_step += 1
        if total_train_step % 100 == 0:  # 逢百才打印记录
            end_time = time.time()
            print(end_time-start_time)
            print("训练次数:{},loss:{}".format(total_train_step, loss.item()))
            writer.add_scalar("train_loss", loss.item(), total_train_step)

    # 测试步骤开始
    tudui.eval()
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():  # 无梯度,不进行调优
        for data in test_dataloader:
            imgs, targets = data
            outputs = tudui(imgs)
            loss = loss_fn(outputs, targets)  # 该loss为部分数据在网络模型上的损失,为tensor数据类型
            # 求整体测试数据集上的误差或正确率
            total_test_loss = total_test_loss + loss.item()  # loss为tensor数据类型,而total_test_loss为普通数字
            # 求整体测试数据集上的误差或正确率
            accuracy = (outputs.argmax(1) == targets).sum()
            #argmax取出指定方向的最大值
            # 1代表:横向比较。==(逻辑比较):True或False,sum:计算True或False个数
            total_accuracy = total_accuracy + accuracy
            #计算总的精确个数
    print("整体测试集上的Loss:{}".format(total_test_loss))
    print("整体测试集上的正确率:{}".format(total_accuracy / test_data_size))
    writer.add_scalar("test_loss", total_test_loss, total_test_step)
    writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step)
    total_test_step += 1

    torch.save(tudui, "tudui_{}.pth".format(i))  # 每一轮保存一个结果
    print("模型已保存")

writer.close()

train_gpu1.py

import torchvision.datasets
from torch.utils.tensorboard import SummaryWriter

from model import *
from torch import nn
from torch.utils.data import DataLoader
import time
# 准备数据集,CIFAR10 数据集是PIL Image,要转换为tensor数据类型
train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=True)

# 看一下训练数据集和测试数据集都有多少张(如何获得数据集的长度)
train_data_size = len(train_data)  # length 长度
test_data_size = len(test_data)
# 如果train_data_size=10,那么打印出的字符串为:训练数据集的长度为:10
print("训练数据集的长度为:{}".format(train_data_size))  # 字符串格式化,把format中的变量替换{}
print("测试数据集的长度为:{}".format(test_data_size))

# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 创建网络模型
tudui = Tudui()
tudui = tudui.cuda()

# 创建损失函数
loss_fn = nn.CrossEntropyLoss()  # 分类问题可以用交叉熵
loss_fn = loss_fn.cuda()

# 定义优化器
learning_rate = 0.01  # 另一写法:1e-2,即1x 10^(-2)=0.01
optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate)  # SGD 随机梯度下降

# 设置训练网络的一些参数
total_train_step = 0  # 记录训练次数
total_test_step = 0  # 记录测试次数
epoch = 10  # 训练轮数

# 添加tensorboard
writer = SummaryWriter("../logs_train_gpu")

start_time = time.time()
for i in range(epoch):
    print("----------第{}轮训练开始-----------".format(i + 1))  # i从0-9
    # 训练步骤开始
    for data in train_dataloader:
        imgs, targets = data
        imgs = imgs.cuda()
        targets = targets.cuda()
        outputs = tudui(imgs)
        loss = loss_fn(outputs, targets)

        # 优化器优化模型
        optimizer.zero_grad()  # 首先要梯度清零
        loss.backward()  # 反向传播得到每一个参数节点的梯度
        optimizer.step()  # 对参数进行优化
        total_train_step += 1
        if total_train_step % 100 == 0:  # 逢百才打印记录
            end_time = time.time()
            print(end_time-start_time)
            print("训练次数:{},loss:{}".format(total_train_step, loss.item()))
            writer.add_scalar("train_loss", loss.item(), total_train_step)

    # 测试步骤开始
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():  # 无梯度,不进行调优
        for data in test_dataloader:
            imgs, targets = data
            imgs = imgs.cuda()
            targets = targets.cuda()
            outputs = tudui(imgs)
            loss = loss_fn(outputs, targets)  # 该loss为部分数据在网络模型上的损失,为tensor数据类型
            # 求整体测试数据集上的误差或正确率
            total_test_loss = total_test_loss + loss.item()  # loss为tensor数据类型,而total_test_loss为普通数字
            # 求整体测试数据集上的误差或正确率
            accuracy = (outputs.argmax(1) == targets).sum()  # 1:横向比较,==:True或False,sum:计算True或False个数
            total_accuracy = total_accuracy + accuracy
    print("整体测试集上的Loss:{}".format(total_test_loss))
    print("整体测试集上的正确率:{}".format(total_accuracy / test_data_size))
    writer.add_scalar("test_loss", total_test_loss, total_test_step)
    writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step)
    total_test_step += 1

    torch.save(tudui, "tudui_{}.pth".format(i))  # 每一轮保存一个结果
    print("模型已保存")

writer.close()

train.gpu2.py(用第二张显卡跑)

import torchvision.datasets
from torch.utils.tensorboard import SummaryWriter

from model import *
from torch import nn
from torch.utils.data import DataLoader
import time

#定义训练的设备
device = torch.device("cuda:1")
print(device)
# 准备数据集,CIFAR10 数据集是PIL Image,要转换为tensor数据类型
train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=True)

# 看一下训练数据集和测试数据集都有多少张(如何获得数据集的长度)
train_data_size = len(train_data)  # length 长度
test_data_size = len(test_data)
# 如果train_data_size=10,那么打印出的字符串为:训练数据集的长度为:10
print("训练数据集的长度为:{}".format(train_data_size))  # 字符串格式化,把format中的变量替换{}
print("测试数据集的长度为:{}".format(test_data_size))

# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 创建网络模型
tudui = Tudui()
tudui = tudui.to(device)

# 创建损失函数
loss_fn = nn.CrossEntropyLoss()  # 分类问题可以用交叉熵
loss_fn = loss_fn.to(device)

# 定义优化器
learning_rate = 0.01  # 另一写法:1e-2,即1x 10^(-2)=0.01
optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate)  # SGD 随机梯度下降

# 设置训练网络的一些参数
total_train_step = 0  # 记录训练次数
total_test_step = 0  # 记录测试次数
epoch = 10  # 训练轮数

# 添加tensorboard
writer = SummaryWriter("../logs_train_gpu")

start_time = time.time()
for i in range(epoch):
    print("----------第{}轮训练开始-----------".format(i + 1))  # i从0-9
    # 训练步骤开始
    for data in train_dataloader:
        imgs, targets = data
        imgs = imgs.to(device)
        targets = targets.to(device)
        outputs = tudui(imgs)
        loss = loss_fn(outputs, targets)

        # 优化器优化模型
        optimizer.zero_grad()  # 首先要梯度清零
        loss.backward()  # 反向传播得到每一个参数节点的梯度
        optimizer.step()  # 对参数进行优化
        total_train_step += 1
        if total_train_step % 100 == 0:  # 逢百才打印记录
            end_time = time.time()
            print(end_time-start_time)
            print("训练次数:{},loss:{}".format(total_train_step, loss.item()))
            writer.add_scalar("train_loss", loss.item(), total_train_step)

    # 测试步骤开始
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():  # 无梯度,不进行调优
        for data in test_dataloader:
            imgs, targets = data
            imgs = imgs.to(device)
            targets = targets.to(device)
            outputs = tudui(imgs)
            loss = loss_fn(outputs, targets)  # 该loss为部分数据在网络模型上的损失,为tensor数据类型
            # 求整体测试数据集上的误差或正确率
            total_test_loss = total_test_loss + loss.item()  # loss为tensor数据类型,而total_test_loss为普通数字
            # 求整体测试数据集上的误差或正确率
            accuracy = (outputs.argmax(1) == targets).sum()  # 1:横向比较,==:True或False,sum:计算True或False个数
            total_accuracy = total_accuracy + accuracy
    print("整体测试集上的Loss:{}".format(total_test_loss))
    print("整体测试集上的正确率:{}".format(total_accuracy / test_data_size))
    writer.add_scalar("test_loss", total_test_loss, total_test_step)
    writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step)
    total_test_step += 1

    torch.save(tudui, "tudui_{}.pth".format(i))  # 每一轮保存一个结果
    print("模型已保存")

writer.close()

21. 完整的模型验证(测试、demo)套路

核心:利用已经训练好的模型,给它提供输入进行测试

以VGG16为例,进行验证

测试过程细节说明:

1.训练数据的标签数据

2.如果训练数据是随便在网络上找图片,需要通过 Resize() 使图片符合模型

3.网络要求是四维的输入 [batch_size,channel,length,width],但是获得的图片是三维的,图片没有指定 batch_size(网络训练过程中是需要 batch_size 的,而图片输入是三维的,需要reshape() 一下。torch.reshape() 方法

4.如果挑选的测试图片预测错误,原因可能是训练次数不够多,将训练轮数 epoch 修改大一点,并且加载更后面的训练文件

5.使用三张随机图片进行测试:

完整测试代码(以bird为例):

test.py

验证图像:

代码:

# 如何从test.py文件去找到dog文件(相对路径)
import torch
import torchvision.transforms
from PIL import Image
from torch import nn

image_path = "../imgs/bird.png"  # 或右键-> Copy Path-> Absolute Path(绝对路径)
# 读取图片(PIL Image),再用ToTensor进行转换
image = Image.open(image_path)  # 现在的image是PIL类型
print(image)  # <PIL.PngImagePlugin.PngImageFile image mode=RGB size=430x247 at 0x1DF29D33AF0>

# image = image.convert('RGB')
# 因为png格式是四通道,除了RGB三通道外,还有一个透明度通道,所以要调用上述语句保留其颜色通道
# 当然,如果图片本来就是三颜色通道,经过此操作,不变
# 加上这一步后,可以适应 png jpg 各种格式的图片,不同截图软件保存的图片通道数不同,也许不加这行代码并不会报错

# 该image大小为260x180,网络模型的输入只能是32x32,进行一个Resize()
# Compose():把transforms几个变换联立在一起
transform = torchvision.transforms.Compose([torchvision.transforms.Resize((32, 32)),  # 32x32大小的PIL Image
                                            torchvision.transforms.ToTensor()])  # 转为Tensor数据类型
image = transform(image)
print(image.shape)  # torch.Size([3, 32, 32])

# 然后加载网络模型
model = torch.load("tudui_29_gpu1.pth")  # 因为test.py和tudui_29_gpu1.pth在同一个层级下,所以地址可以直接写
print(model)
image = torch.reshape(image, (1, 3, 32, 32))
image = image.cuda() #这一步很容易错,采用GPU训练得到的,在验证时一定也要将输入转换到cuda上面
model.eval()  # 模型转化为测试类型
with torch.no_grad():# 节约内存和性能
    output = model(image)
print(output)
train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
print(train_data.classes[output.argmax(1)]) #为了输出具体类别

输出结果:

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~|Bernard|

你的鼓励是我写下去最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值