当前位置:   article > 正文

深入浅出PyTorch: pytorch教程_pytorch 教程

pytorch 教程

目录

1. pytorch tutorial

1.1 pytorch installation

1.1.1 Anaconda/miniconda installation

1.1.2 创建虚拟环境

1.1.3 change package source

1.1.4 查看显卡

1.1.5 install pytorch

1.1.6  pytorch 相关学习资源

1.2 pytorch 基础知识

1.2.1 tensor introduction

1.2.2 自动求导: autograd package

1.2.3 计算梯度:backward()方法

1.2.4 CUDA 并行计算

1.3 torch.cuda()方法

1.3.1 CUDA,Compute Unified Device Architecture 

1.3.2 CUDA functions

torch.cuda.is_available()

torch.to(device)

torch.cuda.empty_caches()

TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

1.3.3 AI硬件加速设备

1.4 pytorch主要模块

1.4.1 基本配置

1.4.2 数据读入: Dataset读取、变换; DataLoader批加载数据

1.4.3 NN模型构建 model

1.4.4 nn.Parameter

1.4.5 模型初始化

1.4.6 损失函数

1.4.7 优化器optimizer

1.4.8 评价指标 

1.4.9 训练和评估

1.4.10 可视化 visualization

2. pytorch基础实战: FashionMNIST

2.1 首先导入必要的包 

2.2 配置训练环境和超参数

2.3 数据读入和加载

2.3.1 数据变换

2.3.2 数据读取方式一

2.3.3 数据读取方式二

2.3.4 模型设计

2.3.5 设定损失函数

2.3.6 设定优化器

2.3.7 训练和验证

 2.3.8 模型保存

3. Extension: pytorch模型定义 

3.1 三种模型定义方式

3.1.1 Sequential

3.1.2 ModuleList

3.1.3 ModuleDict

3.1.4 三种方法的比较与适用场景 

3.2 使用模型块快速搭建复杂网络

3.3 pytorch修改模型 

3.3.1 修改模型层

3.3.2 添加外部输入

 3.3.3 添加额外输出

3.4 pytorch模型保存与读取

3.4.1 单卡保存+单卡加载

 3.4.2 单卡保存+多卡加载

3.4.3 多卡保存+单卡加载

3.4.4 多卡保存+多卡加载

4. Extension: pytorch进阶训练技巧

4.1 自定义损失函数

4.1.1 以函数方式定义

4.1.2 以类方式定义

4.2 动态调整学习率

4.2.1 使用官方API

4.2.2 自定义scheduler

4.3 模型微调-torchvision、timm

4.3.1 训练特定层

4.4 半精度训练

4.5 数据增强-imgaug

4.6 使用argparse进行调参

4.6.1 argparse的使用

4.6.2 更加高效使用argparse修改超参数 

5. Extension: pytorch可视化 

5.1 可视化网络结构 

6. pytorch生态

7. 使用ONNX进行部署并推理

参考


1. pytorch tutorial

1.1 pytorch installation

PyTorch的安装是我们学习PyTorch的第一步,也是经常出错的一步。在安装PyTorch时,我们通常使用的是Anaconda/miniconda+Pytorch+ IDE 的流程

在数据科学和最近很火的深度学习中,要用到大量成熟的package。我们一个个安装 package 很麻烦,而且很容易出现包之间的依赖不适配的问题

Anaconda/miniconda的出现很好的解决了我们的问题,它集成了常用于科学分析(机器学习, 深度学习)的大量package,并且借助于conda我们可以实现对虚拟Python环境的管理

1.1.1 Anaconda/miniconda installation

电脑用anaconda;服务器用miniconda 

登陆Anaconda | Individual Edition,选择相应系统DownLoad,此处以Windows为例(Linux可以点击链接选择合适的版本进行下载或者通过官方提供的shell脚本进行下载)

在开始页找到Anaconda Prompt,一般在Anaconda3的文件夹下,双击运行。

  • Linux在终端下,用bash命令运行shell文件。

bash Miniconda3-latest-Linux-x86_64.sh

Note 注意! Miniconda3最后一步是指定安装环境!可以安装文件转移动其他路径,类似于windows不装在默认的c盘里!使用没有影响! 

  •  查看miniconda安装路径
conda info --root

1.1.2 创建虚拟环境

Linux在终端(Ctrl+Alt+T)进行, Windows在Anaconda Prompt进行

查看现存虚拟环境: conda env list

Note 注意!!!anaconda env经常启动失败所以要首先明确我们现在正在使用的python和pip是指向哪个env,是启动失败是的本地server home:~/.local/bin/pip,还是启动成功时的conda env:~/miniconda3/bin/pip.

  • 这涉及到python packages是否被能被安装到正确路径!!
  • 查看当前运行环境和路径!
    • ​​​​查看当前python版本和安装路径:python --version;which python
    • 查看pip指令安装路径:which pip

创建虚拟环境的命令。

创建anaconda虚拟环境:conda create -n env_name python==version

# 注:将env_name 替换成你的环境的名称,version替换成对应的版本号,eg:3.8

  • 激活环境命令:conda activate env_name
  • 安装miniconda后,miniconda指令地址会自动覆盖server home本地路径,指向自己
    • 不要随便动配置文件,容易造成混乱!导致pip、bash路径指向出错
    • 可能方法:echo 'export PATH="~/anaconda3/bin:$PATH"' >> ~/.bashrc source .bashrc
  • 查看python packages安装路径
    • python; import numpy; print(numpy.__file__)
    • pip show package
  • 安装包packages:conda 和 pip
    • pip 和 conda一般都是将packages安装在lib\site-packages目录下
    • 一种方式:conda install package_name, e.g. conda install matplotlib==2.0.2
      • 不推荐!conda安装torch引起的!通过anaconda-navigator安装pytorch 时它将自动安装cpu和gpu两个版本,因此产生多版本冲突错误
      • 所以一定要用pip来安装,否则会遇到版本冲突error。
    • 另一种方式:pip 定向给指定路径安装packages
      • conda install pip;pip install numpy==1.22.0
      • pip install -t D:\python3.5(32bit)\Lib\site-packages numpy==1.22.0
    • Note:pip和conda都会将package安装到miniconda,但会独立表示,容易安装多个相同package,造成多版本torch或cuda冲突error!e.g. torch_sparse/_version_cpu.so: undefined symbol: _ZN5torch3jit17parseSchemaOrNameERKSs
  • 卸载包:conda remove package_name
  • 删除虚拟环境命令:conda remove -n env_name --all
  • 退出当前环境:conda deactivate
    • 用pip查看server本地python运行环境:pip list。
    • 注意比较pip list 列出的server home本地运行环境与conda list列出的虚拟运行环境env是否一致!可信的是:激活虚拟环境,用conda list查看虚拟运行环境配置。

关于更多的命令,我们可以查看Anaconda/miniconda官方提供的命令,官网链接:点击这里

1.1.3 change package source

在安装package时,我们经常会使用pip install package_nameconda install package_name 的命令,但是一些package下载速度会很慢,因此我们需要进行换源,换成国内源,加快我们的下载速度。

  • 单次下载换源

pip install package_name -i https://pypi.tuna.tsinghua.edu.cn/simple

  • pip换源

Linux下的换源,我们首先需要在用户目录下新建文件夹.pip,并且在文件夹内新建文件pip.conf,具体命令如下

cd ~

mkdir .pip/

vi pip.conf

随后,我们需要在pip.conf添加下方的内容:

[global]

index-url = http://pypi.douban.com/simple

[install]

use-mirrors =true

mirrors =http://pypi.douban.com/simple/

trusted-host =pypi.douban.com

 Windows下换源:

        1) 文件管理器文件路径地址栏敲:%APPDATA% 回车,快速进入 C:\Users\电脑用户\AppData\Roaming 文件夹中

        2) 新建 pip 文件夹并在文件夹中新建 pip.ini 配置文件

        3) 我们需要在pip.ini 配置文件内容,我们可以选择使用记事本打开,输入以下内容,并按下ctrl+s保存,在这里我们使用的是豆瓣源为例子。

[global]

index-url = http://pypi.douban.com/simple

[install]

use-mirrors =true

mirrors =http://pypi.douban.com/simple/

trusted-host =pypi.douban.com

1.1.4 查看显卡

cmd/terminal中输入nvidia-smi(Linux和Win命令一样),查看自己是否有NVIDIA的独立显卡及其型号

Note that我们需要看下版本号,看自己可以兼容的CUDA版本,等会安装PyTorch时是可以向下兼容的。具体适配表如下图所示。

1.1.5 install pytorch

使用conda下载或者pip下载(建议conda安装)

  •  在线下载

打开Terminal,输入conda activate env_name(env_name 为你对应的环境名称),切换到对应的环境下面,我们就可以进行PyTorch的安装了。

Note

Stable代表的是稳定版本,Preview代表的是先行版本

可以结合电脑是否有显卡,选择CPU版本还是CUDA版本,CUDA版本需要拥有独显且是NVIDIA的GPU

官方建议我们使用Anaconda/miniconda来进行管理

关于安装的系统要求

  • Windows

    1. Windows 7及更高版本;建议使用Windows 10或者更高的版本

    2. Windows Server 2008 r2 及更高版本

  • Linux:以常见的CentOS和Ubuntu为例

    1. CentOS, 最低版本7.3-1611

    2. Ubuntu, 最低版本 13.04,这里会导致cuda安装的最大版本不同

  • macOS

    1. macOS 10.10及其以上

有些电脑所支持的cuda版本<10.2,此时我们需要进行手动降级,即就是cudatoolkit = 你所适合的版本,但是这里需要注意下一定要保持PyTorch和cudatoolkit的版本适配。查看Previous PyTorch Versions | PyTorch

  • 离线下载

1.1.6  pytorch 相关学习资源

  1. Awesome-pytorch-list:目前已获12K Star,包含了NLP,CV,常见库,论文实现以及Pytorch的其他项目。

  2. PyTorch官方文档:官方发布的文档,十分丰富。

  3. Pytorch-handbook:GitHub上已经收获14.8K,pytorch手中书。

  4. PyTorch官方社区:PyTorch拥有一个活跃的社区,在这里你可以和开发pytorch的人们进行交流。

  5. PyTorch官方tutorials:官方编写的tutorials,可以结合colab边动手边学习

  6. 动手学深度学习:动手学深度学习是由李沐老师主讲的一门深度学习入门课,拥有成熟的书籍资源和课程资源,在B站,Youtube均有回放。

  7. Awesome-PyTorch-Chinese:常见的中文优质PyTorch资源

  8. labml.ai Deep Learning Paper Implementations:手把手实现经典网络代码

  9. YSDA course in Natural Language Processing:YSDA course in Natural Language Processing

  10. huggingface:hugging face

  11. ModelScope: 魔搭社区

1.2 pytorch 基础知识

1.2.1 tensor introduction

在PyTorch中, torch.Tensor 是存储和变换数据的主要工具。如果你之前用过NumPy,你会发现 Tensor 和NumPy的多维数组非常类似。然而,Tensor 提供GPU计算和自动求梯度等更多功能,这些使 Tensor 这一数据类型更加适合深度学习

几何代数中定义的张量是基于向量和矩阵的推广,比如我们可以将标量视为零阶张量,矢量可以视为一阶张量,矩阵就是二阶张量。

张量维度

代表含义

0维张量

代表的是标量(数字)

1维张量

代表的是向量

2维张量

代表的是矩阵

3维张量 

时间序列数据 股价 文本数据 单张彩色图片(RGB)

4维图像
5维视频

1.2.2 自动求导: autograd package

pytorch中,所有神经网络的核心是autograd package,也就是自动微分。autograd包为张量上所有操作提供了自动求导机制。它是一个在运行时定义(define-by-run)框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。

torch.Tensor是这个包的核心类。如果设置它的属性.requires_grad为True,那么它将会追踪对于该张量的所有操作。当完成计算后可以调用.backward()来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。-->注意:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零

Note,在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor

.detach()方法,要防止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。

with torch.no_grad()方法,为了防止跟踪历史记录,可以将代码块包装在with torch.no_grad():中。在评估模型时特别有用,因为模型可能具有requires_grad=True的可训练参数,但是我们不需要在此过程中对它们进行梯度计算。

还有一个类对于autograd的实现非常重要:Function。Tensor和Function互相连接生成了一个无环图(acyclic graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建Tensor自身的Function(除非这个tensor是用户手动创建的,即这个张量的grad_fn是None)

1.2.3 计算梯度:backward()方法

如果需要计算导数(梯度),可以在Tensor上调用.backward()方法。如果Tensor是一个标量,则不需要为backward()指定任何参数,但如果它有更多的元素,则需要指定一个gradient参数,该参数是形状匹配的张量。

因此out.backward() out.backward(torch.tensor(1.)) 等价

  1. # 创建一个张量并设置requires_grad=True用来追踪其计算历史
  2. x = torch.ones(2, 2, requires_grad=True)
  3. print(x)
  4. tensor([[1., 1.],
  5. [1., 1.]], requires_grad=True)
  6. # 对这个张量做一次运算:
  7. y = x**2
  8. print(y) # y是计算的结果,所以它有grad_fn属性。
  9. tensor([[1., 1.],
  10. [1., 1.]], grad_fn=<PowBackward0>)
  11. # 对 y 进行更多操作
  12. z = y * y * 3
  13. out = z.mean()
  14. print(z, out)
  15. tensor([[3., 3.],
  16. [3., 3.]], grad_fn=<MulBackward0>) tensor(3., grad_fn=<MeanBackward0>)
  17. # 现在开始进行反向传播
  18. out.backward()
  19. # 输出导数 d(out)/dx
  20. print(x.grad)
  21. tensor([[3., 3.],
  22. [3., 3.]])

1.2.4 CUDA 并行计算

pytorch做DL过程中,可能会遇到数据量较大无法在单块GPU上完成,或者需要提升计算速度的场景,这时就需要用到并行计算--多个GPU来参与训练,减少训练时间。

为什么需要CUDA?

CUDA是GPU提供商NVIDA提供的GPU并行计算框架。对于GPU本身的编程,使用的是CUDA语言来实现的。

在pytorch中,使用CUDA表示要开始要求我们的模型或数据开始使用GPU了。

在编写程序中,当我们使用了.cuda()时,其功能是让我们的model或data从CPU迁移到GPU(0)当中,通过GPU开始计算。

Note

  • 我们使用GPU时使用的是.cuda()而不是.gpu()。这是因为当前GPU的编程接口采用CUDA,但是市面上的GPU并不是都支持CUDA,只有部分NVIDA的GPU才支持,AMD的GPU编程接口采用的是OpenCL,在现阶段pytorch并不支持。
  • 数据在GPU和CPU之间传递时会比较耗时,我们应当尽量避免数据的切换。
  • GPU运算很快,但是在使用简单的操作时,我们应该尽量使用CPU去完成。
  • 当我们的服务器上有多个GPU,我们应该指明我们使用的GPU是哪一块,如果我们不设置的话,tensor.cuda()方法会默认将tensor保存到第一块GPU上,等价于tensor.cuda(0),这将会导致out of memory的错误。

常见的并行方法

(详见原文)

设置CUDA加速训练

我们可以通过一下两种方式设置显卡

1.3 torch.cuda()方法

1.3.1 CUDA,Compute Unified Device Architecture 

本质:CUDA是用于深度学习模型,在cpu和GPU之间交换数据的并行计算架构。

简单粗暴的应用:将model running涉及到的model和tensors都传送到显卡上!!

建议使用model.to(device)的方式,将数据和模型送入GPU中。这样可以显示指定需要使用的计算资源,特别是有多个GPU的情况下。

  1. # CUDA GPU 设置方式
  2. # 方案一:使用os.environ,这种情况如果使用GPU不需要设置
  3. import os
  4. os.environ['CUDA_VISIBLE_DEVICES'] = '0,1' # 指明调用的GPU为0,1号
  5. -------------------------------------------------------
  6. # 方案二:使用“device”,后续对要使用GPU的变量用.to(device)即可
  7. device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu") # 指明调用的GPU为1号
  8. # CUDA GPU 调用方式
  9. # 方案一:model.cuda()
  10. -------------------------------------------------------
  11. # 方案二:model.to(device)

1.3.2 CUDA functions

torch.cuda.is_available()

-》判断当前运行环境是否成功调用显卡GPU和CUDA()

torch.to(device)

-》将vector变量copy一份到device指定的GPU上,之后的运算都在GPU上进行。

torch.cuda.empty_caches()

-》释放显存

TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

原因: GPU CUDA 只支持tensor操作,不支持numpy操作

将tensor从显卡传回本地 -》tensor.cpu()

1.3.3 AI硬件加速设备

  • TPU, Tensor Processing Unit。google为为优化自身的Tensorflow框架而打造的一款计算神经网络专用芯片。
  • NPU, Neural-network Processing Unit。“数据驱动并行计算”的架构,中国寒武纪。

1.4 pytorch主要模块

机器学习任务的几个重要步骤:

  • 数据预处理。数据格式的统一、异常数据的消除、必要的数据变换,同时划分训练集、验证集和测试集,常见的方法包括:按比例随机选取、KFold方法(可以使用sklearn中的test_train_split函数、kfold来是实现)
  • 模型选择。
  • 设定损失函数和优化方法、超参数。sklearn中自带的损失函数和优化器。
  • 模型训练
  • 模型验证/测试。在验证/测试集上计算模型表现。

深度学习与机器学习在流程上类似,在代码上有较大差异

  • 由于深度学习所需的样本量很大,一次加载全部数据运行可能会超出内存容量而无法实现;
  • 同时还有batch训练等提高模型表现的策略,需要每次训练读取固定数量的样本送入模型中训练,因此deep learning在数据加载上需要有专门的设计

深度学习与机器学习在模型表现上的差异:

  • 深度神经网络往往需要“逐层”搭建,或者预先定义好可以实现特定功能的模块,再把这些模块组装起来。

损失函数和优化器要能够保证反向传播能够在用户自行定义的模型上实现。

程序默认是在cpu上运行的。因此在代码实现中,需要把模型和数据“放到”GPU上去做运算,同时还需要保证损失函数和优化器能够在GPU上工作。

深度学习中训练和验证过程最大的特点在于读入数据是按批的,每次读入一个批次的数据,放入GPU中训练,然后将损失函数反向传播回网络最前面的层,同时使用优化器调整网络参数。这里会涉及到各个模块配合的问题。训练/验证后还需要根据设定好的指标计算模型表现

1.4.1 基本配置

导包和超参数设置

  1. import os
  2. import numpy as np
  3. import torch
  4. import torch.nn as nn
  5. from torch.utils.data import Dataset, DataLoader
  6. import torch.optim as optimizer
  7. batch_size = 16
  8. # 批次的大小
  9. lr = 1e-4
  10. # 优化器的学习率
  11. max_epochs = 100

1.4.2 数据读入: Dataset读取、变换; DataLoader批加载数据

pytorch数据读入是通过Dataset + DataLoader的方式完成的

  • Dataset定义好数据的格式和数据变换形式,
  • DataLoader用ieterative的方式不断入读批次数据。

我们可以定义自己的Dataset类来实现灵活的数据读取,定义的类需要继承pytorch自身的Dataset类。主要包含三个函数:

  • __init__: 用于向类中传入外部参数,同时定义样本集。
  • __getitem__: 用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据。
  • __len__: 用于返回数据集的样本数。
  1. class MyDataset(Dataset):
  2. def __init__(self, data_dir, info_csv, image_list, transform=None):
  3. """
  4. Args:
  5. data_dir: path to image directory.
  6. info_csv: path to the csv file containing image indexes
  7. with corresponding labels.
  8. image_list: path to the txt file contains image names to training/validation set
  9. transform: optional transform to be applied on a sample.
  10. """
  11. label_info = pd.read_csv(info_csv)
  12. image_file = open(image_list).readlines()
  13. self.data_dir = data_dir
  14. self.image_file = image_file
  15. self.label_info = label_info
  16. self.transform = transform
  17. def __getitem__(self, index):
  18. """
  19. Args:
  20. index: the index of item
  21. Returns:
  22. image and its labels
  23. """
  24. image_name = self.image_file[index].strip('\n')
  25. raw_label = self.label_info.loc[self.label_info['Image_index'] == image_name]
  26. label = raw_label.iloc[:,0]
  27. image_name = os.path.join(self.data_dir, image_name)
  28. image = Image.open(image_name).convert('RGB')
  29. if self.transform is not None:
  30. image = self.transform(image)
  31. return image, label
  32. def __len__(self):
  33. return len(self.image_file)

构建好Dataset后,就可以使用DataLoader来按批次读入数据了,实现代码如下:

  1. from torch.utils.data import DataLoader
  2. train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size,
  3. num_workers=4, shuffle=True, drop_last=True)
  4. val_loader = torch.utils.data.DataLoader(val_data, batch_size=batch_size,
  5. num_workers=4, shuffle=False)
  •  batch_size: 样本是按“批”读入的,batch_size就是每次读入的样本数
  • num_workers: 有多少个进程用于读取数据,windows下该参数设置为0,linux下常见的为4或8,根据自己的电脑配置来设置。
  • shuffle: 是否将读入的数据打乱,一般在选training set中设置为True,在validation set中设置为False。
  • drop_last: 对于样本最后一部分没有达到批次数的样本,使其不在参与训练。

查看加载的数据。pytorch中DataLoader读取的数据可以使用next和iter来完成。

  1. import matplotlib.pyplot as plt
  2. images, labels = next(iter(val_loader))
  3. print(images.shape)
  4. plt.imshow(images[0].transpose(1,2,0))
  5. plt.show()

1.4.3 NN模型构建 model

Note that:

  • 定义模型时,要注意区分:数据、参数。参数是指w/b/u、activation、softmax等需要预先定义的参数/layer/function/module参数只有在__init__()函数中预先定义好,pytorch才能在创建class对象时将参数自动添加到params list中,形成一个可以BP追踪的完整计算流程。除了torch.matmul、torch.add、torch.nn.functional等少数几个函数不需要外,其他都要在__init__()函数中声明。
  • nn.Module定义的函数一个只能用一次。需要重复使用的场景,则需要创建多个相同的函数,用ModuleList([])、Sequential([])存放起来备用

pytorch中的神经网络模型一般是基于nn.Module类的模型来完成的,它让模型构造更灵活。

Module类是torch.nn模块里提供的一个模型构造类,是所有神经网络模块的基类,我们可以继承它来定义我们想要的模型。下面是继承Module类构造的多层感知机。这里定义的MLP类重载了Module类的__init__函数和forward函数。他们分别用于创建模型参数和定义前向计算(正向传播)。

e.g. 一个具有两个隐藏层的多层感知机。

  1. import torch
  2. from torch import nn
  3. class MLP(nn.Module):
  4. # 声明带有模型参数的层,这里声明了两个全连接层
  5. def __init__(self, **kwargs):
  6. # 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
  7. super(MLP, self).__init__(**kwargs)
  8. self.hidden = nn.Linear(784, 256)
  9. self.act = nn.ReLU()
  10. self.output = nn.Linear(256,10)
  11. # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
  12. def forward(self, x):
  13. o = self.act(self.hidden(x))
  14. return self.output(o)

上述MLP类中无需定义反向传播函数。系统将通过自动求梯度而自动生成反向传播所需的backward函数

我们可以实例化MLP类得到模型变量net。下面的代码初始化net并传入数据x做一次前向计算。其中,net(x)会调用MLP继承自Module类的__call__函数,这个函数将调用MLP类定义的forward函数来完成前向计算。因此我们自己构造模型时需要明确定义模型的forward过程。

  1. X = torch.rand(2,784) # 设置一个随机的输入张量
  2. net = MLP() # 实例化模型
  3. print(net) # 打印模型
  4. net(X) # 前向计算

MLP(
  (hidden): Linear(in_features=784, out_features=256, bias=True)
  (act): ReLU()
  (output): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[ 0.0149, -0.2641, -0.0040,  0.0945, -0.1277, -0.0092,  0.0343,  0.0627,
         -0.1742,  0.1866],
        [ 0.0738, -0.1409,  0.0790,  0.0597, -0.1572,  0.0479, -0.0519,  0.0211,
         -0.1435,  0.1958]], grad_fn=<AddmmBackward>)

1.4.4 nn.Parameter

background:当我们的网络有一些其他的设计时,会需要一些额外的参数同样跟着整个网络的训练进行学习更新,最后得到最优的值

本质:torch.nn.Parameter(Tensor)的输入是tensor变量,用于生成参数矩阵W将一个固定不可训练的tensor转化成一个可以训练改变的vector(即parameter),并将parameter绑定到这个module里面

  • Requires_grad=True表示可以训练(改变),False表示值不可改变

nn.Parameter类其实是Tensor的子类,所以它也会被自动记录计算历史和反向传播,如果一个Tensor是Parameter,那么它会自动被添加到模型的参数列表里。所以在自定义含模型参数的层时,我们应该将参数定义成Parameter,除了直接定义成Parameter类外,还可以使用ParameterList和ParameterDict分别定义参数的列表和字典。

  1. class MyListDense(nn.Module):
  2. def __init__(self):
  3. super(MyListDense, self).__init__()
  4. self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4))
  5. for i in range(3)])
  6. self.params.append(nn.Parameter(torch.randn(4, 1)))
  7. def forward(self, x):
  8. for i in range(len(self.params)):
  9. x = torch.mm(x, self.params[i])
  10. return x
  11. net = MyListDense()
  12. print(net)
  1. class MyDictDense(nn.Module):
  2. def __init__(self):
  3. super(MyDictDense, self).__init__()
  4. self.params = nn.ParameterDict({
  5. 'linear1': nn.Parameter(torch.randn(4, 4)),
  6. 'linear2': nn.Parameter(torch.randn(4, 1))
  7. })
  8. self.params.update({'linear3': nn.Parameter(torch.randn(4, 2))}) # 新增
  9. def forward(self, x, choice='linear1'):
  10. return torch.mm(x, self.params[choice])
  11. net = MyDictDense()
  12. print(net)
  • 二维卷积层

二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数草扩列卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。 

  1. import torch
  2. from torch import nn
  3. # 卷积运算(二维互相关)
  4. def corr2d(X, K):
  5. h, w = K.shape
  6. X, K = X.float(), K.float()
  7. Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
  8. for i in range(Y.shape[0]):
  9. for j in range(Y.shape[1]):
  10. Y[i, j] = (X[i: i + h, j: j + w] * K).sum()
  11. return Y
  12. # 二维卷积层
  13. class Conv2D(nn.Module):
  14. def __init__(self, kernel_size):
  15. super(Conv2D, self).__init__()
  16. self.weight = nn.Parameter(torch.randn(kernel_size))
  17. self.bias = nn.Parameter(torch.randn(1))
  18. def forward(self, x):
  19. return corr2d(x, self.weight) + self.bias

1.4.5 模型初始化

在深度学习模型中,权重的初始化极为重要。一个好的初始值,会使模型收敛速度提高,使模型准确率更加精确。一般情况下,我们不适用全为0初始值训练网络。为了利于训练和减少收敛时间,我们需要对模型进行合理的初始化。

torch.nn.init为我们提供了常用的初始化方法。

  • 带有下划线后缀的函数,意味着他们可以直接原地更改输入张量的值。

通过访问torch.nn.init的官方文档链接 ,我们发现torch.nn.init提供了以下初始化方法: 1 . torch.nn.init.uniform_(tensor, a=0.0, b=1.0) 2 . torch.nn.init.normal_(tensor, mean=0.0, std=1.0) 3 . torch.nn.init.constant_(tensor, val) 4 . torch.nn.init.ones_(tensor) 5 . torch.nn.init.zeros_(tensor) 6 . torch.nn.init.eye_(tensor) 7 . torch.nn.init.dirac_(tensor, groups=1) 8 . torch.nn.init.xavier_uniform_(tensor, gain=1.0) 9 . torch.nn.init.xavier_normal_(tensor, gain=1.0) 10 . torch.nn.init.kaiming_uniform_(tensor, a=0, mode='fan__in', nonlinearity='leaky_relu') 11 . torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu') 12 . torch.nn.init.orthogonal_(tensor, gain=1) 13 . torch.nn.init.sparse_(tensor, sparsity, std=0.01) 14 . torch.nn.init.calculate_gain(nonlinearity, param=None) 关于计算增益如下表:

  • torch.nn.init使用

torch.nn.init需要根据实际模型来进行初始化,通常使用isinstance()来进行判断模块类型。

  1. import torch
  2. import torch.nn as nn
  3. conv = nn.Conv2d(1,3,3)
  4. linear = nn.Linear(10,1)
  5. isinstance(conv,nn.Conv2d) # 判断conv是否是nn.Conv2d类型
  6. isinstance(linear,nn.Conv2d) # 判断linear是否是nn.Conv2d类型
  7. # 查看随机初始化的conv参数
  8. conv.weight.data
  9. # 查看linear的参数
  10. linear.weight.data
  11. # 对conv进行kaiming初始化
  12. torch.nn.init.kaiming_normal_(conv.weight.data)
  13. conv.weight.data
  14. # 对linear进行常数初始化
  15. torch.nn.init.constant_(linear.weight.data,0.3)
  16. linear.weight.data
  • 初始化函数的封装 

人们常常将各种初始化方法定义为一个initialize_weights()的函数并在模型初始后进行使用。 

  1. def initialize_weights(self):
  2. for m in self.modules():
  3. # 判断是否属于Conv2d
  4. if isinstance(m, nn.Conv2d):
  5. torch.nn.init.xavier_normal_(m.weight.data)
  6. # 判断是否有偏置
  7. if m.bias is not None:
  8. torch.nn.init.constant_(m.bias.data,0.3)
  9. elif isinstance(m, nn.Linear):
  10. torch.nn.init.normal_(m.weight.data, 0.1)
  11. if m.bias is not None:
  12. torch.nn.init.zeros_(m.bias.data)
  13. elif isinstance(m, nn.BatchNorm2d):
  14. m.weight.data.fill_(1)
  15. m.bias.data.zeros_()
  16. # 模型的定义
  17. class MLP(nn.Module):
  18. # 声明带有模型参数的层,这里声明了两个全连接层
  19. def __init__(self, **kwargs):
  20. # 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
  21. super(MLP, self).__init__(**kwargs)
  22. self.hidden = nn.Conv2d(1,1,3)
  23. self.act = nn.ReLU()
  24. self.output = nn.Linear(10,1)
  25. # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
  26. def forward(self, x):
  27. o = self.act(self.hidden(x))
  28. return self.output(o)
  29. mlp = MLP()
  30. print(list(mlp.parameters()))
  31. print("-------初始化-------")
  32. initialize_weights(mlp)
  33. print(list(mlp.parameters()))

1.4.6 损失函数

模型好的training离不开优质的负反馈,这里的负反馈就是损失函数loss

pytorch常用torch.nn来定义损失函数

--》先解决实际问题的过程中可以进一步探索、借鉴现有工作,设计自己的损失函数

  • 二分类交叉熵损失函数
  • 交叉熵损失函数
  •  L1损失函数。计算输出y和真实标签target之间差值的绝对值。
  • MSE损失函数。用于计算regression问题的损失
  • 平滑L1(Smooth L1)损失函数。减轻离群点带来的影响。
  • 目标破松分布的负对数似然函数
  • KL散度。计算相对熵,用于连续分布的距离度量。
  • MarginRankingLoss。计算两个向量之间的相似度/差异,用于排序任务。
  • 多标签边界损失函数。用于多标签分类问题计算。
  • 二分类损失函数。计算二分类logistic loss。
  • 多分类的折页损失。
  • 三元组损失
  • HingEmbeddingLoss。对输出的embedding结果做Hing损失计算
  • 预先相似度CTC损失函数。用于解决时序类数据的分类。

1.4.7 优化器optimizer

模型优化器 Optimizer

优化器optimizer是根据NN反向传播的梯度信息来更新网络参数,以起到降低loss函数计算值,使得模型输出更加接近真实标签。

Optimizer有三个属性:defaults存储优化器的超参数;state参数的缓存;para_groups管理的参数组,是一个list,其中每个元素是一个字典,顺序是params, lr, momentum, dampening, weight_decay, nesterov。

  • torch.optim.ASGD
  • torch.optim.Adadelta
  • torch.optim.Adagrad
  • torch.optim.Adam
  • torch.optim.AdamW
  • torch.optim.Adamax
  • torch.optim.LBFGS
  • torch.optim.RMSprop
  • torch.optim.Rprop
  • torch.optim.SGD
  • torch.optim.SparseAdam

Optimzer方法

  • zero_grad(): 清空所管理参数的梯度。因为pytorch的特性是张量的梯度不自动清零,所以每次反向传播后都需要清空梯度。
  • step(): 执行一步梯度更新,参数更新。
  • add_param_group(): 添加参数组。
  • load_state_dict(): 加载状态参数字典,可以用来进行模型的断点续训练
  • state_dict(): 获取优化器当前状态信息字典。

Optimizer 使用流程

  1)定义优化器

  2)梯度置零

  3)梯度更新

  1. # 每个优化器都是一个类,我们需要进行实例化才能使用
  2. class Net(nn.Moddule):
  3. ···
  4. net = Net()
  5. optimizer = torch.optim.SGD(net.parameters(), lr=1e-5)
  6. for epoch in range(EPOCH):
  7. ...
  8. optimizer.zero_grad() #梯度置零
  9. loss = ... #计算loss
  10. loss.backward() #BP反向传播
  11. optimizer.step() #梯度更新

Optimizer给网络不同的层赋予不同的优化器参数

  1. from torch import optim
  2. from torchvision.models import resnet18
  3. net = resnet18()
  4. optimizer = optim.SGD([
  5. {'params':net.fc.parameters()},#fc的lr使用默认的1e-5
  6. {'params':net.layer4[0].conv1.parameters(),'lr':1e-2}],lr=1e-5)
  7. # 可以使用param_groups查看属性

 Optimizer实际操作

  1. import os
  2. import torch
  3. # 设置权重,服从正态分布 --> 2 x 2
  4. weight = torch.randn((2, 2), requires_grad=True)
  5. # 设置梯度为全1矩阵 --> 2 x 2
  6. weight.grad = torch.ones((2, 2))
  7. # 输出现有的weight和data
  8. print("The data of weight before step:\n{}".format(weight.data))
  9. print("The grad of weight before step:\n{}".format(weight.grad))
  10. # 实例化优化器
  11. optimizer = torch.optim.SGD([weight], lr=0.1, momentum=0.9)
  12. # 进行一步操作
  13. optimizer.step()
  14. # 查看进行一步后的值,梯度
  15. print("The data of weight after step:\n{}".format(weight.data))
  16. print("The grad of weight after step:\n{}".format(weight.grad))
  17. # 权重清零
  18. optimizer.zero_grad()
  19. # 检验权重是否为0
  20. print("The grad of weight after optimizer.zero_grad():\n{}".format(weight.grad))
  21. # 输出参数
  22. print("optimizer.params_group is \n{}".format(optimizer.param_groups))
  23. # 查看参数位置,optimizer和weight的位置一样,我觉得这里可以参考Python是基于值管理
  24. print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))
  25. # 添加参数:weight2
  26. weight2 = torch.randn((3, 3), requires_grad=True)
  27. optimizer.add_param_group({"params": weight2, 'lr': 0.0001, 'nesterov': True})
  28. # 查看现有的参数
  29. print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
  30. # 查看当前状态信息
  31. opt_state_dict = optimizer.state_dict()
  32. print("state_dict before step:\n", opt_state_dict)
  33. # 进行5次step操作
  34. for _ in range(50):
  35. optimizer.step()
  36. # 输出现有状态信息
  37. print("state_dict after step:\n", optimizer.state_dict())
  38. # 保存参数信息
  39. torch.save(optimizer.state_dict(),os.path.join(r"D:\pythonProject\Attention_Unet", "optimizer_state_dict.pkl"))
  40. print("----------done-----------")
  41. # 加载参数信息
  42. state_dict = torch.load(r"D:\pythonProject\Attention_Unet\optimizer_state_dict.pkl") # 需要修改为你自己的路径
  43. optimizer.load_state_dict(state_dict)
  44. print("load state_dict successfully\n{}".format(state_dict))
  45. # 输出最后属性信息
  46. print("\n{}".format(optimizer.defaults))
  47. print("\n{}".format(optimizer.state))
  48. print("\n{}".format(optimizer.param_groups))

1.4.8 评价指标 

模型评价指标

  • 混淆矩阵
  • Overall Accuracy
  • Average Accuracy
  • Kappa系数
  • Recall
  • Precision
  • F1
  • PR曲线
  • 置信度
  • IOU
  • AP
  • mAP

1.4.9 训练和评估

我们在完成了模型的训练后,需要在测试集/验证集上完成模型的验证,以确保我们的模型具有泛化能力、不会出现过拟合等问题。在PyTorch中,训练和评估的流程是一致的,只是在训练过程中需要将模型的参数进行更新,而在评估过程中则不需要更新参数。 

Learning Target:

  • pytorch的训练/评估模式的开启
  • 完整的训练/评估流程

完成了上述设定后就可以加载数据开始训练模型了。

  • 首先应该设置模型的状态:
    •  如果是训练状态,那么模型的参数应该支持反向传播的修改;
    • 如果是验证/测试状态,则不应该修改模型参数。 

        在pytorch中,模型的状态设置非常简便,如下的两个操作二选一即可:

  1. model.train() # 训练状态
  2. model.eval() # 验证/测试状态
  • 我们前面在DataLoader构建完成后介绍了如何从中读取数据,在训练过程中使用类似的操作即可,区别在于此时要用for循环读取DataLoader中的全部数据
for data, label in train_loader:
  • 之后将数据放到GPU上用于后续计算,此处以.cuda()为例
data, label = data.cuda(), label.cuda()
  • 开始用当前批次数据做训练时,应当先将优化器的梯度置零
optimizer.zero_grad()
  • 之后将data放入模型中训练
output = model(data)
  • 根据预先定义的criterion计算损失函数
loss = criterion(output, label)
  • 将loss反向传播回网络
loss.backward()
  • 使用优化器更新模型参数
optimizer.step()

这样一个训练过程就完成了,对于测试或验证过程,可以计算分类准确率,这部分会在下一节图像分类中介绍。

验证/测试的流程基本与训练过程一致,不同点在于:

  • 需要预先设置torch.no_grad,以及将model调至eval模式
  • 不需要将优化器的梯度置零
  • 不需要将loss反向传播回到网络
  • 不需要更新optimizer

一个完整的图像分类的训练过程如下所示:

  1. def train(epoch):
  2. model.train()
  3. train_loss = 0
  4. for data, label in train_loader:
  5. data, label = data.cuda(), label.cuda()
  6. optimizer.zero_grad()
  7. output = model(data)
  8. loss = criterion(output, label)
  9. loss.backward()
  10. optimizer.step()
  11. train_loss += loss.item()*data.size(0)
  12. train_loss = train_loss/len(train_loader.dataset)
  13. print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, train_loss))

对应的,一个完成图像分类的验证过程如下所示:

with torch.no_grad()

  1. def val(epoch):
  2. model.eval()
  3. val_loss = 0
  4. with torch.no_grad():
  5. for data, label in val_loader:
  6. data, label = data.cuda(), label.cuda()
  7. output = model(data)
  8. preds = torch.argmax(output, 1)
  9. loss = criterion(output, label)
  10. val_loss += loss.item()*data.size(0)
  11. running_accu += torch.sum(preds == label.data)
  12. val_loss = val_loss/len(val_loader.dataset)
  13. print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, val_loss))

1.4.10 可视化 visualization

在pytorch深度学习中,可视化是一个可选项。指的是某些任务在训练完成后,需要对一些必要的内容进行可视化,比如分类的ROC曲线,卷积网络中的卷积核,一个训练/验证过程的损害函数曲线等。

2. pytorch基础实战: FashionMNIST

基础实战——FashionMNIST时装分类 — 深入浅出PyTorch

2.1 首先导入必要的包 

  1. import os
  2. import numpy as np
  3. import pandas as pd
  4. import torch
  5. import torch.nn as nn
  6. import torch.optim as optim
  7. from torch.utils.data import Dataset, DataLoader

2.2 配置训练环境和超参数

  1. # 配置GPU,这里有两种方式
  2. ## 方案一:使用os.environ
  3. os.environ['CUDA_VISIBLE_DEVICES'] = '0'
  4. # 方案二:使用“device”,后续对要使用GPU的变量用.to(device)即可
  5. device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")
  6. ## 配置其他超参数,如batch_size, num_workers, learning rate, 以及总的epochs
  7. batch_size = 256
  8. num_workers = 4 # 对于Windows用户,这里应设置为0,否则会出现多线程错误
  9. lr = 1e-4
  10. epochs = 20

2.3 数据读入和加载

这里同时展示两种方式:

  • 下载并使用PyTorch提供的内置数据集

  • 从网站下载以csv格式存储的数据,读入并转成预期的格式
    第一种数据读入方式只适用于常见的数据集,如MNIST,CIFAR10等,PyTorch官方提供了数据下载。这种方式往往适用于快速测试方法(比如测试下某个idea在MNIST数据集上是否有效)
    第二种数据读入方式需要自己构建Dataset,这对于PyTorch应用于自己的工作中十分重要

2.3.1 数据变换

同时,还需要对数据进行必要的变换,比如说需要将图片统一为一致的大小,以便后续能够输入网络训练;需要将数据格式转为Tensor类,等等。

这些变换可以很方便地借助torchvision包来完成,这是PyTorch官方用于图像处理的工具库,上面提到的使用内置数据集的方式也要用到。PyTorch的一大方便之处就在于它是一整套“生态”,有着官方和第三方各个领域的支持。这些内容我们会在后续课程中详细介绍。

  1. # 首先设置数据变换
  2. from torchvision import transforms
  3. image_size = 28
  4. data_transform = transforms.Compose([
  5. transforms.ToPILImage(),
  6. # 这一步取决于后续的数据读取方式,如果使用内置数据集读取方式则不需要
  7. transforms.Resize(image_size),
  8. transforms.ToTensor()
  9. ])

2.3.2 数据读取方式一

  1. ## 读取方式一:使用torchvision自带数据集,下载可能需要一段时间
  2. from torchvision import datasets
  3. train_data = datasets.FashionMNIST(root='./', train=True, download=True, transform=data_transform)
  4. test_data = datasets.FashionMNIST(root='./', train=False, download=True, transform=data_transform)

2.3.3 数据读取方式二

  1. ## 读取方式二:读入csv格式的数据,自行构建Dataset类
  2. # csv数据下载链接:https://www.kaggle.com/zalando-research/fashionmnist
  3. class FMDataset(Dataset):
  4. def __init__(self, df, transform=None):
  5. self.df = df
  6. self.transform = transform
  7. self.images = df.iloc[:,1:].values.astype(np.uint8)
  8. self.labels = df.iloc[:, 0].values
  9. def __len__(self):
  10. return len(self.images)
  11. def __getitem__(self, idx):
  12. image = self.images[idx].reshape(28,28,1)
  13. label = int(self.labels[idx])
  14. if self.transform is not None:
  15. image = self.transform(image)
  16. else:
  17. image = torch.tensor(image/255., dtype=torch.float)
  18. label = torch.tensor(label, dtype=torch.long)
  19. return image, label
  20. train_df = pd.read_csv("./FashionMNIST/fashion-mnist_train.csv")
  21. test_df = pd.read_csv("./FashionMNIST/fashion-mnist_test.csv")
  22. train_data = FMDataset(train_df, data_transform)
  23. test_data = FMDataset(test_df, data_transform)

在构建训练和测试数据集完成后,需要定义DataLoader类,以便在训练和测试时加载数据

  1. train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True, num_workers=num_workers, drop_last=True)
  2. test_loader = DataLoader(test_data, batch_size=batch_size, shuffle=False, num_workers=num_workers)

读入后,我们可以做一些数据可视化操作,主要是验证我们读入的数据是否正确

  1. import matplotlib.pyplot as plt
  2. image, label = next(iter(train_loader))
  3. print(image.shape, label.shape)
  4. plt.imshow(image[0][0], cmap="gray")
  5. ------------------------------------------
  6. torch.Size([256, 1, 28, 28])
  7. torch.Size([256])
  8. <matplotlib.image.AxesImage at 0x7f19a043cc10>

2.3.4 模型设计

手搭一个CNN,模型构建完成后,将模型放到GPU上用于训练。

  1. class Net(nn.Module):
  2. def __init__(self):
  3. super(Net, self).__init__()
  4. self.conv = nn.Sequential(
  5. nn.Conv2d(1, 32, 5),
  6. nn.ReLU(),
  7. nn.MaxPool2d(2, stride=2),
  8. nn.Dropout(0.3),
  9. nn.Conv2d(32, 64, 5),
  10. nn.ReLU(),
  11. nn.MaxPool2d(2, stride=2),
  12. nn.Dropout(0.3)
  13. )
  14. self.fc = nn.Sequential(
  15. nn.Linear(64*4*4, 512),
  16. nn.ReLU(),
  17. nn.Linear(512, 10)
  18. )
  19. def forward(self, x):
  20. x = self.conv(x)
  21. x = x.view(-1, 64*4*4)
  22. x = self.fc(x)
  23. # x = nn.functional.normalize(x)
  24. return x
  25. model = Net()
  26. model = model.cuda()
  27. # model = nn.DataParallel(model).cuda() # 多卡训练时的写法,之后的课程中会进一步讲解

2.3.5 设定损失函数

使用torch.nn模块自带的CrossEntropy损失
PyTorch会自动把整数型的label转为one-hot型,用于计算CE loss
这里需要确保label是从0开始的,同时模型不加softmax层(使用logits计算),这也说明了PyTorch训练中各个部分不是独立的,需要通盘考虑

  1. criterion = nn.CrossEntropyLoss()
  2. # criterion = nn.CrossEntropyLoss(weight=[1,1,1,1,3,1,1,1,1,1])

2.3.6 设定优化器

optimizer = optim.Adam(model.parameters(), lr=0.001)

2.3.7 训练和验证

各自封装成函数,方便后续调用
关注两者的主要区别:

  • 模型状态设置

  • 是否需要初始化优化器

  • 是否需要将loss传回到网络

  • 是否需要每步更新optimizer

此外,对于测试或验证过程,可以计算分类准确率

  1. def train(epoch):
  2. model.train()
  3. train_loss = 0
  4. for data, label in train_loader:
  5. data, label = data.cuda(), label.cuda()
  6. optimizer.zero_grad()
  7. output = model(data)
  8. loss = criterion(output, label)
  9. loss.backward()
  10. optimizer.step()
  11. train_loss += loss.item()*data.size(0)
  12. train_loss = train_loss/len(train_loader.dataset)
  13. print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, train_loss))
  1. def val(epoch):
  2. model.eval()
  3. val_loss = 0
  4. gt_labels = []
  5. pred_labels = []
  6. with torch.no_grad():
  7. for data, label in test_loader:
  8. data, label = data.cuda(), label.cuda()
  9. output = model(data)
  10. preds = torch.argmax(output, 1)
  11. gt_labels.append(label.cpu().data.numpy())
  12. pred_labels.append(preds.cpu().data.numpy())
  13. loss = criterion(output, label)
  14. val_loss += loss.item()*data.size(0)
  15. val_loss = val_loss/len(test_loader.dataset)
  16. gt_labels, pred_labels = np.concatenate(gt_labels), np.concatenate(pred_labels)
  17. acc = np.sum(gt_labels==pred_labels)/len(pred_labels)
  18. print('Epoch: {} \tValidation Loss: {:.6f}, Accuracy: {:6f}'.format(epoch, val_loss, acc))
  1. for epoch in range(1, epochs+1):
  2. train(epoch)
  3. val(epoch)

 2.3.8 模型保存

训练完成后,可以使用torch.save保存模型参数或者整个模型,也可以在训练过程中保存模型
这部分会在后面的课程中详细介绍

  1. save_path = "./FahionModel.pkl"
  2. torch.save(model, save_path)

3. Extension: pytorch模型定义 

3.1 三种模型定义方式

  • Module类是torch.nn模块里提供的一个模型构造类(nn.Module),是所有神经网络模块的基类,我们可以继承它来定义我们想要的模型
  • pytorch模型定义应包括两个主要部分:各个部分的初始化(__init__);数据流向定义(forward)

基于nn.Module,我们可以通过Sequential, ModuleListModuleDict三种方式定义pytorch模型。

3.1.1 Sequential

对应的模块为nn.Sequential()。

当模型的前向计算为简单串联各个层的计算时,Sequential类可以通过更加简单的方式定义模型。它可以接收一个子模块的有序字典(OrderedDict)或者一系列子模块作为参数来逐一添加Module的实例,而模型的前向计算就是将这些实例按添加的顺序逐一计算。我们结合Sequential和定义方式加以理解:

  1. from collections import OrderedDict
  2. class MySequential(nn.Module):
  3. def __init__(self, *args):
  4. super(MySequential, self).__init__()
  5. if len(args) == 1 and isinstance(args[0], OrderedDict): # 如果传入的是一个OrderedDict
  6. for key, module in args[0].items():
  7. self.add_module(key, module)
  8. # add_module方法会将module添加进self._modules(一个OrderedDict)
  9. else: # 传入的是一些Module
  10. for idx, module in enumerate(args):
  11. self.add_module(str(idx), module)
  12. def forward(self, input):
  13. # self._modules返回一个 OrderedDict,保证会按照成员添加时的顺序遍历成
  14. for module in self._modules.values():
  15. input = module(input)
  16. return input
  • 直接排列
  1. import torch.nn as nn
  2. net = nn.Sequential(
  3. nn.Linear(784, 256),
  4. nn.ReLU(),
  5. nn.Linear(256, 10),
  6. )
  7. print(net)
  8. --------------------------
  9. Sequential(
  10. (0): Linear(in_features=784, out_features=256, bias=True)
  11. (1): ReLU()
  12. (2): Linear(in_features=256, out_features=10, bias=True)
  13. )
  • 使用OrderedDict 
  1. import collections
  2. import torch.nn as nn
  3. net2 = nn.Sequential(collections.OrderedDict([
  4. ('fc1', nn.Linear(784, 256)),
  5. ('relu1', nn.ReLU()),
  6. ('fc2', nn.Linear(256, 10))
  7. ]))
  8. print(net2)
  9. ----------------------------------------
  10. Sequential(
  11. (fc1): Linear(in_features=784, out_features=256, bias=True)
  12. (relu1): ReLU()
  13. (fc2): Linear(in_features=256, out_features=10, bias=True)
  14. )

我们可以看到,使用Sequential定义模型的好处在于简单、易读,同时使用Sequential定义的模型不需要在写forward,因为顺序已经定义好了。但使用Sequential也会使得模型定义丧失灵活性,比如需要在模型中间加入一个外部输入时就不适合用Sequential的方式实现。使用时需要根据实际需求加以选择。 

3.1.2 ModuleList

对应模块为nn.ModuleList()。

ModuleList接收一个子模块(或层,需属于nn.Module类)的列表作为输入,然后也可以类似List那样进行append和extend操作。同时,子模块或层的权重也会自动添加到网络中来

  1. net = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
  2. net.append(nn.Linear(256, 10)) # # 类似List的append操作
  3. print(net[-1]) # 类似List的索引访问
  4. print(net)
  5. ------------------------------------------------------
  6. Linear(in_features=256, out_features=10, bias=True)
  7. ModuleList(
  8. (0): Linear(in_features=784, out_features=256, bias=True)
  9. (1): ReLU()
  10. (2): Linear(in_features=256, out_features=10, bias=True)
  11. )

Note that,nn.ModuleList并没有定义一个网络,它只是将不同的模块储存在一起。Modulelist中元素的先后顺序并不代表其在网络中的真实位置顺序,需要经过forward函数指定各个层的先后顺序后才算完成了模型的定义。具体实现时用for循环可完成:

  1. class model(nn.Module):
  2. def __init__(self, ...):
  3. super().__init__()
  4. self.modulelist = ...
  5. ...
  6. def forward(self, x):
  7. for layer in self.modulelist:
  8. x = layer(x)
  9. return x

3.1.3 ModuleDict

对应模块为nn.ModuleDict()。

ModuleDict和ModuleList的作用类似,只是ModuleDict能够更方便得作为神经网络的层添加名称。

  1. net = nn.ModuleDict({
  2. 'linear': nn.Linear(784, 256),
  3. 'act': nn.ReLU(),
  4. })
  5. net['output'] = nn.Linear(256, 10) # 添加
  6. print(net['linear']) # 访问
  7. print(net.output)
  8. print(net)
  9. ------------------------------------
  10. Linear(in_features=784, out_features=256, bias=True)
  11. Linear(in_features=256, out_features=10, bias=True)
  12. ModuleDict(
  13. (act): ReLU()
  14. (linear): Linear(in_features=784, out_features=256, bias=True)
  15. (output): Linear(in_features=256, out_features=10, bias=True)
  16. )

3.1.4 三种方法的比较与适用场景 

Sequential适用于快速验证结果,因为已经明确了要用哪些层,直接写一下就好了,不需要同时写__init__和forward

ModuleList和Dict在某个完全相同的层需要重复出现多次时,非常方便实现,可以"一行顶多行"。

当我们需要之前层的信息时,比如ResNets中的残差计算,当前层的结果需要和之前层中的结果进行融合,一般使用ModuleList/moduleDict比较方便。

3.2 使用模型块快速搭建复杂网络

problem:当模型深度非常大时,有几百行代码中很多重复出现的结构,使用起来很不方便。

solution:将这些重复出现的层定义为一个“模块”,若干层串联成的“模块”也有其输入和输出。

3.3 pytorch修改模型 

3.3.1 修改模型层

将模型(net)最后名称为"fc"的层替换成了名称为"classifier"的结构。

  1. import torchvision.models as models
  2. net = models.resnet50()
  3. print(net)
  4. # 重新定义一个"模块"
  5. from collections import OrderedDict
  6. classifier = nn.Sequential(OrderedDict([('fc1', nn.Linear(2048, 128)),
  7. ('relu1', nn.ReLU()),
  8. ('dropout1',nn.Dropout(0.5)),
  9. ('fc2', nn.Linear(128, 10)),
  10. ('output', nn.Softmax(dim=1))
  11. ]))
  12. net.fc = classifier

3.3.2 添加外部输入

将原模型作为一个整体参数输入到新模型中,新模型调整好原模型输入与后续添加输入、后续层之间的关系,从而完成模型修改。

  1. import torchvision.models as models
  2. net = models.resnet50()
  3. print(net)
  4. class Model(nn.Module):
  5. def __init__(self, net):
  6. super(Model, self).__init__()
  7. self.net = net
  8. self.relu = nn.ReLU()
  9. self.dropout = nn.Dropout(0.5)
  10. self.fc_add = nn.Linear(1001, 10, bias=True)
  11. self.output = nn.Softmax(dim=1)
  12. def forward(self, x, add_variable):
  13. x = self.net(x)
  14. x = torch.cat((self.dropout(self.relu(x)), add_variable.unsqueeze(1)),1)
  15. x = self.fc_add(x)
  16. x = self.output(x)
  17. return x

 3.3.3 添加额外输出

target:输出模型某一中间层的结果,以施加额外的监督,获得更好的中间层结果

idea:修改模型定义中forward函数的return变量。

  1. import torchvision.models as models
  2. net = models.resnet50()
  3. print(net)
  4. class Model(nn.Module):
  5. def __init__(self, net):
  6. super(Model, self).__init__()
  7. self.net = net
  8. self.relu = nn.ReLU()
  9. self.dropout = nn.Dropout(0.5)
  10. self.fc1 = nn.Linear(1000, 10, bias=True)
  11. self.output = nn.Softmax(dim=1)
  12. def forward(self, x, add_variable):
  13. x1000 = self.net(x)
  14. x10 = self.dropout(self.relu(x1000))
  15. x10 = self.fc1(x10)
  16. x10 = self.output(x10)
  17. return x10, x1000

3.4 pytorch模型保存与读取

pytorch存储模型主要采用pkl,pt,pth三种格式。就使用层面来说并没有区别。

一个pytorch模型主要包含两个部分:模型结构和权重。其中,模型时继承nn.Module的类,权重的数据结构是一个字典(keyi是层名,value是权重向量)。

因此,存储也由此分为两种形式:存储整个模型(包括结构和权重),和只存储模型权重

3.4.1 单卡保存+单卡加载

  1. import os
  2. import torch
  3. from torchvision import models
  4. os.environ['CUDA_VISIBLE_DEVICES'] = '0' #这里替换成希望使用的GPU编号
  5. model = models.resnet152(pretrained=True)
  6. model.cuda()
  7. save_dir = 'resnet152.pt' #保存路径
  8. # 保存+读取整个模型
  9. torch.save(model, save_dir)
  10. loaded_model = torch.load(save_dir)
  11. loaded_model.cuda()
  12. # 保存+读取模型权重
  13. torch.save(model.state_dict(), save_dir)
  14. loaded_model = models.resnet152() #注意这里需要对模型结构有定义
  15. loaded_model.load_state_dict(torch.load(save_dir))
  16. loaded_model.cuda()

 3.4.2 单卡保存+多卡加载

这种情况的处理比较简单,读取单卡保存的模型后,使用nn.DataParallel函数进行分布式训练设置即可(相当于3.1代码中.cuda()替换一下):

  1. import os
  2. import torch
  3. from torchvision import models
  4. os.environ['CUDA_VISIBLE_DEVICES'] = '0' #这里替换成希望使用的GPU编号
  5. model = models.resnet152(pretrained=True)
  6. model.cuda()
  7. # 保存+读取整个模型
  8. torch.save(model, save_dir)
  9. os.environ['CUDA_VISIBLE_DEVICES'] = '1,2' #这里替换成希望使用的GPU编号
  10. loaded_model = torch.load(save_dir)
  11. loaded_model = nn.DataParallel(loaded_model).cuda()
  12. # 保存+读取模型权重
  13. torch.save(model.state_dict(), save_dir)
  14. os.environ['CUDA_VISIBLE_DEVICES'] = '1,2' #这里替换成希望使用的GPU编号
  15. loaded_model = models.resnet152() #注意这里需要对模型结构有定义
  16. loaded_model.load_state_dict(torch.load(save_dir))
  17. loaded_model = nn.DataParallel(loaded_model).cuda()

3.4.3 多卡保存+单卡加载

  1. import os
  2. import torch
  3. from torchvision import models
  4. os.environ['CUDA_VISIBLE_DEVICES'] = '1,2' #这里替换成希望使用的GPU编号
  5. model = models.resnet152(pretrained=True)
  6. model = nn.DataParallel(model).cuda()
  7. # 保存+读取整个模型
  8. torch.save(model, save_dir)
  9. os.environ['CUDA_VISIBLE_DEVICES'] = '0' #这里替换成希望使用的GPU编号
  10. loaded_model = torch.load(save_dir).module

3.4.4 多卡保存+多卡加载

  1. import os
  2. import torch
  3. from torchvision import models
  4. os.environ['CUDA_VISIBLE_DEVICES'] = '0,1,2' #这里替换成希望使用的GPU编号
  5. model = models.resnet152(pretrained=True)
  6. model = nn.DataParallel(model).cuda()
  7. # 保存+读取模型权重,强烈建议!!
  8. torch.save(model.state_dict(), save_dir)
  9. loaded_model = models.resnet152() #注意这里需要对模型结构有定义
  10. loaded_model.load_state_dict(torch.load(save_dir)))
  11. loaded_model = nn.DataParallel(loaded_model).cuda()

4. Extension: pytorch进阶训练技巧

4.1 自定义损失函数

4.1.1 以函数方式定义

  1. def my_loss(output, target):
  2. loss = torch.mean((output - target)**2)
  3. return loss

4.1.2 以类方式定义

因为损失函数的_loss和_WeightedLoss继承自nn.Module,所以我们可以将其当做神经网络的一层来对待。

  1. class DiceLoss(nn.Module):
  2. def __init__(self,weight=None,size_average=True):
  3. super(DiceLoss,self).__init__()
  4. def forward(self,inputs,targets,smooth=1):
  5. inputs = F.sigmoid(inputs)
  6. inputs = inputs.view(-1)
  7. targets = targets.view(-1)
  8. intersection = (inputs * targets).sum()
  9. dice = (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)
  10. return 1 - dice
  11. # 使用方法
  12. criterion = DiceLoss()
  13. loss = criterion(input,targets)

4.2 动态调整学习率

problem: 学习率的选择是DL中困扰人们许久的问题。 

  • 学习率设置过小,会极大降低收敛速度,增加训练时间;学习率太大,可能导致参数在最优解两侧来回震荡。
  • 当我们选定了一个合适的学习率后,经过多伦训练后,可能会出现准确率震荡或loss不再下降等情况,说明当前学习率已不能满足模型调优的需求。 

solution:可以通过一个适当的学习率衰减策略来改善这种现象,提高我们的精度。这种方式在pytorch中称为scheduler。 

4.2.1 使用官方API

  • lr_scheduler.LambdaLR
  • lr_scheduler.MultiplicativeLR
  • lr_scheduler.StepLR
  • lr_scheduler.MultiStepLR
  • lr_scheduler.ExponentialLR
  • lr_scheduler.CosineAnnealingLR
  • lr_scheduler.ReduceLROnPlateau
  • lr_scheduler.CyclicLR
  • lr_scheduler.OneCycleLR
  • lr_scheduler.CosineAnnealingWarmRestarts
  1. # 选择一种优化器
  2. optimizer = torch.optim.Adam(...)
  3. # 选择上面提到的一种或多种动态调整学习率的方法
  4. scheduler1 = torch.optim.lr_scheduler....
  5. scheduler2 = torch.optim.lr_scheduler....
  6. ...
  7. schedulern = torch.optim.lr_scheduler....
  8. # 进行训练
  9. for epoch in range(100):
  10. train(...)
  11. validate(...)
  12. optimizer.step()
  13. # 需要在优化器参数更新之后再动态调整学习率
  14. # scheduler的优化是在每一轮后面进行的
  15. scheduler1.step()
  16. ...
  17. schedulern.step()

4.2.2 自定义scheduler

自定义函数adjust_learning_rate来改变param_group中lr的值。

  1. def adjust_learning_rate(optimizer, epoch):
  2. lr = args.lr * (0.1 ** (epoch // 30))
  3. for param_group in optimizer.param_groups:
  4. param_group['lr'] = lr
  5. optimizer = torch.optim.SGD(model.parameters(),lr = args.lr,momentum = 0.9)
  6. for epoch in range(10):
  7. train(...)
  8. validate(...)
  9. adjust_learning_rate(optimizer,epoch)

4.3 模型微调-torchvision、timm

problem: 在实际应用中,我们数据集可能只有几千张,这时从头开始训练具有几千万参数的大型神经网络是不现实的。

solution:迁移学习的一大应用场景--微调 finetuning。比如pytorch中预训练好的网络模型(VGG,ResNet系列,mobilenet系列)

  1. 在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型。

  2. 创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层跟源数据集的标签紧密相关,因此在目标模型中不予采用。

  3. 为目标模型添加一个输出⼤小为⽬标数据集类别个数的输出层,并随机初始化该层的模型参数。

  4. 在目标数据集上训练目标模型。我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。

4.3.1 训练特定层

在默认情况下,参数的属性.requires_grad=True,如果我们从头开始训练或微调不需要注意这里。但如果我们正在提取特征并且只想为新初始化的层计算梯度,其他参数不进行改变。我们就需要通过设置requires_grad=False来冻结部分层。

  1. def set_parameter_requires_grad(model, feature_extracting):
  2. if feature_extracting:
  3. for param in model.parameters():
  4. param.requires_grad = False
  5. import torchvision.models as models
  6. # 冻结参数的梯度
  7. feature_extract = True
  8. model = models.resnet18(pretrained=True)
  9. set_parameter_requires_grad(model, feature_extract)
  10. # 修改模型
  11. num_ftrs = model.fc.in_features
  12. model.fc = nn.Linear(in_features=num_ftrs, out_features=4, bias=True)

4.4 半精度训练

为了节省显卡显存

pytorch默认的浮点数存储方式是torch.float32,小数点后位数更多固然能保证数据的准确性,但绝大多数场景下不需要这么精确,只保留一半的信息也不会影响结果,即使用torch.float16格式。由于精度减了一半,所以被称为“半精度”。

  • import autocast
from torch.cuda.amp import autocast
  • 模型设置

在模型定义中,使用python的装饰器方法,用autocast装饰模型中的forward函数。关于装饰器的使用,可以参考这里

  1. @autocast()
  2. def forward(self, x):
  3. ...
  4. return x
  •  训练过程

在训练过程中,只需在将数据输入模型及其之后的部分放入“with autocast():“即可:

  1. for x in train_loader:
  2. x = x.cuda()
  3. with autocast():
  4. output = model(x)
  5. ...

4.5 数据增强-imgaug

problem: 为了解决模型过拟合问题

我们可以通过加入正则项或减少模型学习参数来解决

但最直接的避免过拟合方法是增加数据

但有些应用场景下我们无法获得大量数据,针对这种有限limited数据问题的解决方案

数据增强,提高训练数据集的大小和质量,以便我们可以用它们来构建更好的深度学习模型。

4.6 使用argparse进行调参

在服务器上跑模型时,如何更方便的修改超参数

Argparse

4.6.1 argparse的使用

总的来说,我们可以将argparse的使用归纳为以下三个步骤。

  • 创建ArgumentParser()对象

  • 调用add_argument()方法添加参数

  • 使用parse_args()解析参数 在接下来的内容中,我们将以实际操作来学习argparse的使用方法。

  1. # demo.py
  2. import argparse
  3. # 创建ArgumentParser()对象
  4. parser = argparse.ArgumentParser()
  5. # 添加参数
  6. parser.add_argument('-o', '--output', action='store_true',
  7. help="shows output")
  8. # action = `store_true` 会将output参数记录为True
  9. # type 规定了参数的格式
  10. # default 规定了默认值
  11. parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=1e-3')
  12. parser.add_argument('--batch_size', type=int, required=True, help='input batch size')
  13. # 使用parse_args()解析函数
  14. args = parser.parse_args()
  15. if args.output:
  16. print("This is some output")
  17. print(f"learning rate:{args.lr} ")
  18. ---------------------------------------------------------------------------
  19. 我们在命令行使用python demo.py --lr 3e-4 --batch_size 32,就可以看到以下的输出
  20. ----------------------------------------------------------------------------
  21. This is some output
  22. learning rate: 3e-4

argparse的参数主要可以分为可选参数和必选参数。可选参数就跟我们的lr参数相类似,未输入的情况下会设置为默认值。必选参数就跟我们的batch_size参数相类似,当我们给参数设置required =True后,我们就必须传入该参数,否则就会报错。

4.6.2 更加高效使用argparse修改超参数 

  1. import argparse
  2. def get_options(parser=argparse.ArgumentParser()):
  3. parser.add_argument('--workers', type=int, default=0,
  4. help='number of data loading workers, you had better put it '
  5. '4 times of your gpu')
  6. parser.add_argument('--batch_size', type=int, default=4, help='input batch size, default=64')
  7. parser.add_argument('--niter', type=int, default=10, help='number of epochs to train for, default=10')
  8. parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=1e-3')
  9. parser.add_argument('--seed', type=int, default=118, help="random seed")
  10. parser.add_argument('--cuda', action='store_true', default=True, help='enables cuda')
  11. parser.add_argument('--checkpoint_path',type=str,default='',
  12. help='Path to load a previous trained model if not empty (default empty)')
  13. parser.add_argument('--output',action='store_true',default=True,help="shows output")
  14. opt = parser.parse_args()
  15. if opt.output:
  16. print(f'num_workers: {opt.workers}')
  17. print(f'batch_size: {opt.batch_size}')
  18. print(f'epochs (niters) : {opt.niter}')
  19. print(f'learning rate : {opt.lr}')
  20. print(f'manual_seed: {opt.seed}')
  21. print(f'cuda enable: {opt.cuda}')
  22. print(f'checkpoint_path: {opt.checkpoint_path}')
  23. return opt
  24. if __name__ == '__main__':
  25. opt = get_options()

我们可以在training文件中用argparse调用、修改参数 

  1. # 导入必要库
  2. ...
  3. import config
  4. opt = config.get_options()
  5. manual_seed = opt.seed
  6. num_workers = opt.workers
  7. batch_size = opt.batch_size
  8. lr = opt.lr
  9. niters = opt.niters
  10. checkpoint_path = opt.checkpoint_path
  11. # 随机数的设置,保证复现结果
  12. def set_seed(seed):
  13. torch.manual_seed(seed)
  14. torch.cuda.manual_seed_all(seed)
  15. random.seed(seed)
  16. np.random.seed(seed)
  17. torch.backends.cudnn.benchmark = False
  18. torch.backends.cudnn.deterministic = True
  19. ...
  20. if __name__ == '__main__':
  21. set_seed(manual_seed)
  22. for epoch in range(niters):
  23. train(model,lr,batch_size,num_workers,checkpoint_path)
  24. val(model,lr,batch_size,num_workers,checkpoint_path)

5. Extension: pytorch可视化 

5.1 可视化网络结构 

随着深度神经网络做的的发展,网络的结构越来越复杂,我们也很难确定每一层的输入结构,输出结构以及参数等信息,这样导致我们很难在短时间内完成debug。因此掌握一个可以用来可视化网络结构的工具是十分有必要的。类似的功能在另一个深度学习库Keras中可以调用一个叫做model.summary()的API来很方便地实现,调用后就会显示我们的模型参数,输入大小,输出大小,模型的整体参数等,但是在PyTorch中没有这样一种便利的工具帮助我们可视化我们的模型结构。 

torchinfo 

  • torchinfo的安装
  1. # 安装方法一
  2. pip install torchinfo
  3. # 安装方法二
  4. conda install -c conda-forge torchinfo
  • torchinfo的使用

trochinfo的使用也是十分简单,我们只需要使用torchinfo.summary()就行了,必需的参数分别是model,input_size[batch_size,channel,h,w]。

  1. import torchvision.models as models
  2. from torchinfo import summary
  3. resnet18 = models.resnet18() # 实例化模型
  4. summary(resnet18, (1, 3, 224, 224)) # 1:batch_size 3:图片的通道数 224: 图片的高宽
  • CNN可视化

  • tensorflow可视化。TensorBoard工具

6. pytorch生态

  • torchvision简介。计算机视觉
  • PyTorchVideo。专注于视频理解工作的深度学习库
  • torchtext。NLP工具包

7. 使用ONNX进行部署并推理

深度学习的最终目的是要实现模型的部署以方便我们的生活和解决传统方法不能解决的问题。通常人们会将模型部署在手机端、开发板,嵌入式设备上,但是这些设备上由于框架的规模,环境依赖,算力的限制,我们无法直接使用训练好的权重进行推理

我们会将PyTorch训练好的模型转换为ONNX( Open Neural Network Exchange)  格式,然后使用ONNX Runtime运行它进行推理。 

参考

3.7 训练和评估 — 深入浅出PyTorch

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/721622
推荐阅读
相关标签
  

闽ICP备14008679号