torch学习 -- pytorch问题大全

pytorch问题大全

本文目录

  • pytorch问题大全
    • 一、pytorch的简单说明
    • 二、pytorch resnet专题
      • resnet18
      • BasicBlock和Bottleneck的区别
    • 三、add_module()
    • 四、torch.nn.functional实现插值和上采样
    • 五、torch.randn和torch.rand有什么区别
    • 六、torch.max()
    • 七、torch.logsumexp()
    • 八、torch.save()
    • 九、torch cuda
      • 检查自己的系统是否支持CUDA
      • pytorch中torch.cuda基础入门以及简单使用
      • pytorch中GPU与CPU的运算性能比较
      • pytorch中GPU与CPU的相互转化
    • 十、Pytorch autograd, backward详解
      • 需要重新认识tensor
    • 十一、transforms.Compose()
    • 十二、torch.nn.funtiona模块与torch.nn模块
    • 十三、Pytorch-BN层
      • BN解决了Internal Covariate Shift问题
      • BN的具体步骤
      • Pytorch中的BN

实验室定期的学习交流会,整理一份我自己主讲的pytorch框架的笔记,主要针对看论文学习过程中遇到的一些问题,因为之前是学的tensorflow

一、pytorch的简单说明

1️⃣PyTorch 的设计遵循tensorvariable(autograd)nn.Module

2️⃣三个由低到高的抽象层次,分别代表高维数组(张量)自动求导(变量)神经网络(层/模块),而且这三个抽象之间联系紧密,可以同时进行修改和操作

3️⃣PyTorch的源码只有TensorFlow的十分之一左右,更少的抽象、更直观的设计使得PyTorch的源码更易于阅读

特点

  • PyTorch 提供了运行在 GPU/CPU 之上、基础的张量操作库;
  • 可以内置的神经网络库;
  • 提供模型训练功能;
  • 支持共享内存的多进程并发(multiprocessing )库等;
  • 处于机器学习第一大语言 Python 的生态圈之中,使得开发者能使用广大的 Python 库和软件;如 NumPy、SciPy 和 Cython(为了速度把 Python 编译成 C 语言);
  • (最大优势)改进现有的神经网络,提供了更快速的方法——不需要从头重新构建整个网络,这是由于 PyTorch 采用了动态计算图(dynamic computational graph)结构,而不是大多数开源框架(TensorFlow、Caffe、CNTK、Theano 等)采用的静态计算图;
  • 提供工具包,如torchtorch.nntorch.optim等;

Pytorch常用工具包

  • torch :类似 NumPy 的张量库,强 GPU 支持 ;
  • torch.autograd :基于 tape 的自动区别库,支持 torch 之中的所有可区分张量运行;
  • torch.nn :为最大化灵活性未涉及、与 autograd 深度整合的神经网络库;
  • torch.optim:与 torch.nn 一起使用的优化包,包含 SGD、RMSProp、LBFGS、Adam 等标准优化方式;
  • torch.multiprocessing: python 多进程并发,进程之间 torch Tensors 的内存共享;
  • torch.utils:数据载入器。具有训练器和其他便利功能;
  • torch.legacy(.nn/.optim) :处于向后兼容性考虑,从 Torch 移植来的 legacy 代码;

二、pytorch resnet专题

resnet18

数字代表的是网络的深度,这里的18指定的是带有权重的18层,包括卷积层和全连接层,不包括池化层和BN层

从Resnet论文中的这张图可以看出,Resnet18由17个卷积层+1个全连接层组成

无论哪一种resnet,除了公共部分(conv1)外,都是由4大块组成,con2x, con3x, con4x, con5x

我笔记里面提到的动态区就是这4大块,静态区就是conv1

BasicBlock和Bottleneck的区别

1️⃣ BasicBlock 两层的残差块 resnet18/34

class BasicBlock(nn.Module):
    """
    
    """
    def __init__(self, inplanes, planes, stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        
        self.relu = nn.ReLU(inplace=True)
        
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        
    # 重要的forward函数
    def forward(self, x):
        residual = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        
        out = self.conv2(out)
        out = self.bn2(out)
        out += residual
        out = self.relu(out)
        return out
        

2️⃣ Bottleneck 三层的残差块 resnet50/101/152

class Bottleneck(nn.Module):
    def __init__(self, inplanes, planes, stride=1, downsample=None):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, stride=stride, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        
        self.conv3 = nn.Conv2d(planes, planes*4, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(planes*4)
        
        self.relu = nn.ReLU(inplace=True)
        
    def forward(self, x):
        residual = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        
        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)
        
        out = self.conv3(out)
        out = self.bn3(out)
        out += residual
        out = self.relu(out)
        return out
    

三、add_module()

为什么要用add_module()函数

  1. 某些pytorch项目,需要动态调整结构。比如简单的三层全连接 l 1 , l 2 , l 3 l1, l2, l3 l1,l2,l3,在训练几个epoch后根据loss选择将全连接 l 2 l2 l2 替换为其它结构 l 2 ′ l2′ l2
  2. 使用了别人编写的pytorch代码,希望快速地将模型中的特定结构替换掉而不改动别人的源码。

什么是add_module()函数

可以看到,是Module类的成员函数,输入参数为

Module.add_module(name: str, module: Module)

功能为,为Module添加一个子module,对应名字为name。

怎么用add_module()函数

现在回忆一下,一般定义模型时,Module A的子module都是在A.init(self)中定义的,比如A中一个卷积子模块self.conv1 = torch.nn.Conv2d(…)。此时,这个卷积模块在A的名字其实是’conv1’。

对比之下,add_module()函数就可以在A.init(self以外定义A的子模块。如定义同样的卷积子模块,可以通过A.add_module(‘conv1’, torch.nn.Conv2d(…))

以上是给A添加一个子模块。那么删除就是del A.conv1。而替换同样采用add_module()函数,只要name与被替换模块相同即可完成替换。如之前已经定义了A.conv1,但此时希望将其替换为新的自定义模块NewOne(torch.nn.Module),只需要A.add_module(‘conv1’, NewOne())即可。

注意事项

  1. 如果是替换,只要保证前后(如这里的torch.nn.Conv2d与NewOne)的forward输入输出维度一致,就可以不用改写A.forward()。如果是增减,则需要考虑重写A.forward()

  2. 如果使用了cuda,并且多卡,需要将model放回cpu后进行结构修改。

例子

class ConvLayer(nn.Module):
    def __init__(self):
        self.add_module('conv',nn.Conv2d(...))
        self.add_module('bn',nn.BatchNorm2d(...))


四、torch.nn.functional实现插值和上采样

语法:

torch.nn.functional.interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None)

input – 输入张量

size – 输出大小

scale_factor – 指定输出为输入的多少倍数。如果输入为tuple,其也要制定为tuple类型

mode – 可使用的上采样算法,有’nearest’, ‘linear’, ‘bilinear’, ‘bicubic’ , ‘trilinear’和’area’,默认使用’nearest’

align_corners – 几何上,我们认为输入和输出的像素是正方形,而不是点。如果设置为True,则输入和输出张量由其角像素的中心点对齐,从而保留角像素处的值。如果设置为False,则输入和输出张量由它们的角像素的角点对齐,插值使用边界外值的边值填充;当scale_factor保持不变时,使该操作独立于输入大小。仅当使用的算法为’linear’, ‘bilinear’, 'bilinear’or 'trilinear’时可以使用。默认设置为False

根据给定的 size 或 scale_factor 参数来对输入进行下/上采样

使用的插值算法取决于参数 mode 的设置


五、torch.randn和torch.rand有什么区别

一个均匀分布,一个是标准正态分布。

均匀分布

torch.rand(*sizes, out=None) → Tensor

返回一个张量,包含了从区间[0, 1)的均匀分布中抽取的一组随机数。张量的形状由参数sizes定义。

参数:

  • sizes (int…) - 整数序列,定义了输出张量的形状
  • out (Tensor, optinal) - 结果张量

例子:

torch.rand(2, 3)
0.0836 0.6151 0.6958
0.6998 0.2560 0.0139
[torch.FloatTensor of size 2x3]

标准正态分布

torch.randn(*sizes, out=None) → Tensor

返回一个张量,包含了从标准正态分布(均值为0,方差为1,即高斯白噪声)中抽取的一组随机数。张量的形状由参数sizes定义。

参数:

  • sizes (int…) - 整数序列,定义了输出张量的形状
  • out (Tensor, optinal) - 结果张量

例子:

torch.randn(2, 3)
0.5419 0.1594 -0.0413
-2.7937 0.9534 0.4561
[torch.FloatTensor of size 2x3]

六、torch.max()

在分类问题中,通常需要使用max()函数对softmax函数的输出值进行操作,求出预测值索引。下面讲解一下torch.max()函数的输入及输出值都是什么。

torch.max() 返回最大值

1️⃣

torch.max(input) → Tensor

返回输入tensor中所有元素的最大值

a = torch.randn(1, 3)
>>0.4729 -0.2266 -0.2085
 
torch.max(a)
>>0.4729

2️⃣

torch.max(input, dim, keepdim=False, out=None) -> (Tensor, LongTensor)

按维度dim 返回最大值

输入

  • input是softmax函数输出的一个tensor
  • dim是max函数索引的维度0/10是每列的最大值,1是每行的最大值

输出

  • 函数会返回两个tensor,第一个tensor是每行的最大值;第二个tensor是每行最大值的索引(位置)。

例如,一个tensor a 是这样的:

tensor([[ 1,  5, 62, 54],
        [ 2,  6,  2,  6],
        [ 2, 65,  2,  6]])

索引每行的最大值:

torch.max(a, 1)

输出:

torch.return_types.max(
values=tensor([62,  6, 65]),   # 每一行的最大值分别是62 6 65
indices=tensor([2, 3, 1])    # 他们的位置索引分别是2 3 1
)  

七、torch.logsumexp()

torch.logsumexp(input, dim, keepdim=False, out=None)
  • input ([Tensor] – the input tensor.
  • dim ([int]) or tuple of python:ints) – the dimension or dimensions to reduce. 要减小的尺寸
  • keepdim ([bool]) – whether the output tensor has dim retained or not. 输出张量是否保持dim
  • out ([Tensor], optional) – the output tensor.

这是一个计算数学公式的函数

l o g s u m e x p ( x ) i = l o g ∑ j e x p ( x i j ) logsumexp(x)_i=log\sum_{j}exp(x_{ij}) logsumexp(x)i=logjexp(xij)
返回给定维度中输入张量的总指数对数

如果keepdimTrue,则输出张量的大小与input相同,但尺寸为dim的大小为 1。否则,压缩dim(请参见 [torch.squeeze()],导致输出张量的尺寸减少 1(或len(dim))。

>>> a = torch.randn(3, 3)
>>> torch.logsumexp(a, 1)
tensor([ 0.8442,  1.4322,  0.8711])

八、torch.save()

torch.save(state, dir)

state是一个字典,保存三个参数:

state = {‘net':model.state_dict(), 'optimizer':optimizer.state_dict(), 'epoch':epoch}

dir表示保存文件的绝对路径+保存文件名,如'/home/qinying/Desktop/modelpara.pth'


九、torch cuda

在GPU中使用torch.cuda进行训练可以大幅提升深度学习运算的速度. 而且 Torch有一套很好的GPU运算体系.可以大大的提升我们的元算速度,特别是当我们进行大数据的运算时,今天我们来讲解以及分析一下pytorch使用CUDA,视频教程可以参考GPU 加速运算


检查自己的系统是否支持CUDA

  • 首先你的电脑里必须得有合适的GPU显卡(NVIDIA),且支持CUDA模块. GPU支持请参考NVIDIA官网

  • 必须安装GPU版的Torch,即安装pytorch时指定了cuda,安装教程参考使用Pip/conda/source在Ubuntu/centos/Mac Os中安装Pytorch v0.2教程

  • 说了这么多,到底如何查看当前环境是否支持CUDA呢?只需要:

    print torch.cuda.is_available()
    # 返回True代表支持,False代表不支持
    

pytorch中torch.cuda基础入门以及简单使用

pytorch中的torch.cuda基础入门在文档中其实已经讲得很详细了,比如我们使用torch.cuda.synchronize()可以等待当前设备上所有流中的所有内核完成。同时我们还可以使用NVIDIA工具扩展(NVTX),还有很多用法,这里就不一一介绍了,具体查看torch.cuda中文文档


pytorch中GPU与CPU的运算性能比较

有很多朋友说在使用GPU和CPU进行运算的过程中(比如GAN),发现使用的时间都差不多;是不是GPU并不比CPU快多少呢?

其实不是这样,如果你运行一个很小的数据模型,那么CPU和GPU的运算速度是差不多的,但是如果你运行大型模型,就可以看到加速效果。我们不能单纯说GPU一定比CPU快,决定因素除了除了我们GPU的配置,还有我们的网络,数据的大小以及数据的类型,有时候GPU运算反而不如CPU快速

举例说明:在使用的情况下,在Titan X GPU中运行VGG16比在Dual Xeon E5-2630 v3 CPU中快66倍


pytorch中GPU与CPU的相互转化

  • 深度学习中我们默认使用的是CPU,如果我们要使用GPU,需要使用.cuda将计算或者数据从CPU移动至GPU,

  • 如果当我们需要在CPU上进行运算时,比如使用plt可视化绘图, 我们可以使用.cpu将计算或者数据转移至CPU.

    import torch
    from torch.autograd import Variable
    
    # 将变量或者数据移到GPU
    gpu_info = Variable(torch.randn(3,3)).cuda()
    # 将变量或者数据移到CPU
    cpu_info = gpu_info.cpu()
    

十、Pytorch autograd, backward详解

PyTorch中,所有神经网络的核心是 autograd 包。先简单介绍一下这个包,然后训练我们的第一个的神经网络。

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

需要重新认识tensor

torch.Tensor是这个包的核心类。如果设置它的属性 .requires_gradTrue,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用 .backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性.

另外一个Tensor中通常会记录如下图中所示的属性:

  • data: 即存储的数据信息

  • requires_grad: 设置为True则表示该Tensor需要求导

  • grad: 该Tensor的梯度值,每次在计算backward时都需要将前一时刻的梯度归零,否则梯度值会一直累加,这个会在后面讲到。

  • grad_fn: 叶子节点通常为None,只有结果节点的grad_fn才有效,用于指示梯度函数是哪种类型。例如上面示例代码中的

 y.grad_fn=<PowBackward0 at 0x213550af048>, z.grad_fn=<AddBackward0 at 0x2135df11be0>
  • is_leaf: 用来指示该Tensor是否是叶子节点。

还有一个类对于autograd的实现非常重要:Function

TensorFunction 互相连接生成了一个无圈图(acyclic graph),它编码了完整的计算历史。每个张量都有一个 .grad_fn 属性,该属性引用了创建 Tensor 自身的Function(除非这个张量是用户手动创建的,即这个张量的 grad_fnNone )。

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

import torch

创建一个张量并设置requires_grad=True用来追踪其计算历史

x = torch.ones(2, 2, requires_grad=True)
print(x)

输出:

tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

十一、transforms.Compose()

transforms.Compose([transforms.ToTensor(), 
                    transforms.Normalize(std=(0.5, 0.5, 0.5),mean=(0.5, 0.5, 0.5))])

则其作用就是先将输入归一化到(0,1),再使用公式”(x-mean)/std”

0.8 变成 (0.8-0.5)/0.5

经过,这一波操作以后,数据分布到 (-1,1之间了)

前面的(0.5,0.5,0.5)是R G B三个通道上的均值,后面(0.5, 0.5, 0.5)是三个通道的标准差

各有3个是因为处理的是RGB三通道的彩色图像是对数据做归一化

这两个tuple数据是用来对RGB 图像做归一化的


十二、torch.nn.funtiona模块与torch.nn模块

# 一般都有这个 F
import torch.nn.functional as F

def forward(self, x):
    x = self.pool(F.relu(self.conv1(x)))
    x = self.pool(F.relu(self.conv2(x)))

torch.nn.Module中实现layer的都是一个特殊的类,他们都是以class xxx来定义的,会自动提取可学习的参数

nn.functional中的函数,由def function( )定义

简单来说,就是functional中的函数是一个确定的不变的运算公式,输入数据产生输出就ok

而深度学习中会有很多权重是在不断更新的,不可能每进行一次forward就用新的权重重新来定义一遍函数来进行计算

如果模型有可学习的参数,最好使用nn.Module对应的相关layer,否则二者都可以使用,没有什么区别

F函数通常不含有weight。 比如relu 就没有weight。


十三、Pytorch-BN层

BN解决了Internal Covariate Shift问题

机器学习领域有个很重要的假设:独立同分布假设,**即假设训练数据和测试数据是满足相同分布的。**我们知道:神经网络的训练实际上就是在拟合训练数据的分布。如果不满足独立同分布假设,那么训练得到的模型的泛化能力肯定不好。

再来思考一个问题:为什么传统的神经网络要求将数据归一化(训练阶段将训练数据归一化并记录均值和方差,测试阶段利用记录的均值和方差将测试数据也归一化)?

首先:做了归一化之后,可以近似的认为训练数据和测试数据满足相同分布(即均值为0,方差为1的标准正态),这样一来模型的泛化能力会得到提高。其次:如果不做归一化,使用mini-batch梯度下降法训练的时候,每批训练数据的分布不相同,那么网络就要在每次迭代的时候去适应不同的分布,这样会大大降低网络的训练速度。综合以上两点,所以需要对数据做归一化预处理。PS:如果是mini-batch梯度下降法,每个batch都可以计算出一个均值和方差,最终记录的均值和方差是所有batches均值和方差的期望,当然也有其它更复杂的记录方式,如pytorch使用的滑动平均。

Internal Covariate Shift问题:在训练的过程中,即使对输入层做了归一化处理使其变成标准正态,随着网络的加深,函数变换越来越复杂,许多隐含层的分布还是会彻底放飞自我,变成各种奇奇怪怪的正态分布,并且整体分布逐渐往非线性函数(也就是激活函数)的取值区间的上下限两端靠近。对于sigmoid函数来说,就意味着输入值是大的负数或正数,这导致反向传播时底层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因。

为了解决上述问题,又想到网络的某个隐含层相对于之后的网络就相当于输入层,所以BN的基本思想就是:把网络的每个隐含层的分布都归一化到标准正态。其实就是把越来越偏的分布强制拉回到比较标准的分布,这样使得激活函数的输入值落在该激活函数对输入比较敏感的区域,这样一来输入的微小变化就会导致损失函数较大的变化。通过这样的方式可以使梯度变大,就避免了梯度消失的问题,而且梯度变大意味着收敛速度快,能大大加快训练速度。

简单说来就是:传统的神经网络只要求第一个输入层归一化,而带BN的神经网络则是把每个输入层(把隐含层也理解成输入层)都归一化。


BN的具体步骤

BN实际上包含两步操作。 x i x_i xi 是BN的输入, y i y_i yi 是BN的输出。

1.归一化到标准正态 ϵ \epsilon ϵ 是一个非常小的数字,是为了防止除以0
μ = 1 m ∑ i = 1 m x i \mu=\frac{1}{m}\sum\limits_{i=1}^{m}x_i μ=m1i=1mxi

σ 2 = 1 m ∑ i = 1 m ( x i − μ ) 2 \sigma^2=\frac{1}{m}\sum\limits_{i=1}^{m}(x_i-\mu)^2 σ2=m1i=1m(xiμ)2

x ^ i ← x i − μ σ 2 + ϵ \hat{x}_i\larr\frac{x_i-\mu}{\sqrt{\sigma^2+\epsilon}} x^iσ2+ϵ xiμ

以sigmoid函数为例,可以将其近似的看成两个部分,中间区域的线性部分以及两侧的非线性部分。Internal Covariate Shift问题就是:隐含层的输出都落在了sigmoid函数的非线性区域,这部分区域对损失函数的影响极小,所以梯度也极小。归一化操作就是把非线性区域的值拉回到线性区域,这样一来虽然增大了梯度,但也降低了数据的非线性表示能力。所以还需要缩放操作来弥补归一化操作降低的非线性表达能力。归一化从形式上看来就是把输入值减去一个数字,再除以一个数字。它的逆操作就是先乘以一个数字,在加上一个数字,这就是缩放。

2.缩放
y i ← γ x i ˆ + β ≡ B N γ , β ( x i ) y i ← γ x i ^ + β ≡ B N γ , β ( x i ) yi←γxiˆ+β≡BNγ,β(xi)y_i\larr\gamma\hat{x_i}+\beta\equiv BN_{\gamma,\beta}(x_i) yiγxiˆ+βBNγ,β(xi)yiγxi^+βBNγ,β(xi)


Pytorch中的BN

Pytorch中的BN操作为

nn.BatchNorm2d(self, num_features, eps=1e-5, momentum=0.1, affine=True, track_running_stats=True)
  • num_features,输入数据的通道数,归一化时需要的均值和方差是在每个通道中计算的
  • eps,用来防止归一化时除以0
  • momentum,滑动平均的参数,用来计算running_meanrunning_var
  • affine,是否进行仿射变换,即缩放操作
  • track_running_stats,是否记录训练阶段的均值和方差,即running_meanrunning_var

BN层的状态包含五个参数:

  • weight,缩放操作的 γ \gamma γ
  • bias,缩放操作的 β \beta β
  • running_mean,训练阶段统计的均值,测试阶段会用到。
  • running_var,训练阶段统计的方差,测试阶段会用到。
  • num_batches_tracked,训练阶段的batch的数目,如果没有指定momentum,则用它来计算running_mean和running_var。一般momentum默认值为0.1,所以这个属性暂时没用。

weightbias这两个参数需要训练,而running_mean、running_val和num_batches_tracked不需要训练,它们只是训练阶段的统计值。


pytorch中的BN继承自:

class _BatchNorm(Module):

这个类的代码:

 class _BatchNorm(Module):
 
    def __init__(self, num_features, eps=1e-5, momentum=0.1, affine=True,
                 track_running_stats=True):
        super(_BatchNorm, self).__init__()
        self.num_features = num_features
        self.eps = eps
        self.momentum = momentum
        self.affine = affine
        self.track_running_stats = track_running_stats
        if self.affine:
            self.weight = Parameter(torch.Tensor(num_features))
            self.bias = Parameter(torch.Tensor(num_features))
        else:
            self.register_parameter('weight', None)
            self.register_parameter('bias', None)
        if self.track_running_stats:
            self.register_buffer('running_mean', torch.zeros(num_features))
            self.register_buffer('running_var', torch.ones(num_features))
        else:
            self.register_parameter('running_mean', None)
            self.register_parameter('running_var', None)
        self.reset_parameters()
 
    def reset_parameters(self):
        if self.track_running_stats:
            self.running_mean.zero_()
            self.running_var.fill_(1)
        if self.affine:
            self.weight.data.uniform_()
            self.bias.data.zero_()
 
    def forward(self, input):
        self._check_input_dim(input)
 
        return F.batch_norm(
            input, self.running_mean, self.running_var, self.weight, self.bias,
            self.training or not self.track_running_stats, self.momentum, self.eps)

你可能感兴趣的:(ML,/,DL,python,pytorch,深度学习)