PyTorch学习笔记之张量

1,272 阅读14分钟

PyTorch学习笔记之张量

1.1 张量的概念

  1. 标量(scalar):0阶张量
  • 只有大小没有方向的数据
  • 例如温度、路程、电流等
  1. 向量(矢量):1阶张量
  • 基于笛卡尔坐标系(x、y、z轴)
  • 基向量:单位长度为1,方向为坐标轴方向
  • 以位移为基础,可将某两点间的位移作为标准,引入一阶张量的概念
  1. 矩阵:2阶张量
  • 通过应力分析引入二阶张量
  • 张量的阶数即需要多少个基本向量以完整的求解张量的每个部件
  1. 3阶张量:公用数据存储在张量
  • 时间序列数据
  • 股价
  • 文本数据
  • 彩色图片(RGB)
  1. m维张量可以看作m-1维张量在某个方向上的堆叠

  2. 存储在各种类型张量的公用数据集类型:

  • 3维 = 时间序列
  • 4维 = 图像
  • 5维 = 视频
  • 例如:
    • 一个图像可以用三个字段表示:(width, height, channel)=3D

    • 但在深度学习中,要处理的不止一张图片或文档,而要处理一个集合,故要使用4D张量:(sample_size, width, height, channel)=4D

1.2 生成张量

1.2.1 随机初始化矩阵

import torch
x = torch.rand(4, 3) 
print(x)
  • 输出:
tensor([[0.7569, 0.4281, 0.4722],
        [0.9513, 0.5168, 0.1659],
        [0.4493, 0.2846, 0.4363],
        [0.5043, 0.9637, 0.1469]])

1.2.2 全0矩阵的构建

  1. 基本方法
import torch
x = torch.zeros(4, 3, dtype=torch.long)
print(x)
  • 输出:
tensor([[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]])
  1. 将现有矩阵转换为全0矩阵
torch.zero_()
torch.zeros_like()

1.2.3 基于已存在的张量创建

x = x.new_ones(4, 3, dtype=torch.double) 
# 创建一个新的全1矩阵tensor,返回的tensor默认具有相同的torch.dtype和torch.device
# 也可以像之前的写法 x = torch.ones(4, 3, dtype=torch.double)
print(x)
x = torch.randn_like(x, dtype=torch.float)
# 重置数据类型
print(x)
# 结果会有一样的size
# 获取它的维度信息
print(x.size())
print(x.shape)
  • 输出
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]], dtype=torch.float64)
tensor([[ 2.7311, -0.0720,  0.2497],
        [-2.3141,  0.0666, -0.5934],
        [ 1.5253,  1.0336,  1.3859],
        [ 1.3806, -0.6965, -1.2255]])
torch.Size([4, 3])
torch.Size([4, 3])
  • 返回的torch.Size是一个tuple,支持所有tuple的操作。可以使用索引操作取得张量的长、宽等数据维度

1.2.4 其他常见构造方法

函数功能
Tensor(sizes)基础构造函数
tensor(data)类似于np.array
ones(sizes)全1
zeros(sizes)全0
eye(sizes)对角为1,其余为0
arange(s,e,step)从s到e,步长为step
linspace(s,e,steps)从s到e,均匀分成step份
rand/randn(sizes)rand是[0,1)均匀分布;randn是服从N(0,1)的正态分布
normal(mean,std)正态分布(均值为mean,标准差是std)
randperm(m)随机排列
  • 举例
# 构造一个无初始定义的张量
tensor_2 = torch.empty((3, 3)) #默认为float32
# 构造一个元素全为1的矩阵
tensor_5 = torch.ones(3,3,dtype=torch.float)
  • 输出
tensor([[0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.]])
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])
  • 如果构建时不声明张量的数据类型,一般会默认为float32

1.2.5 张量与Numpy数据间的转换

  1. 代码示例
import torch
import numpy as np

# tensor转numpy
t1 = torch.randn((3,3)) # 构建3×3元素随机的tensor
t1_numpy = t1.numpy()

# numpy转tensor
t1_numpy = np.array([[2,3],[4,5],[6,7]]) # 构建numpy格式的矩阵
t1_torch = torch.from_numpy(t1_numpy)
  1. 转换过程不影响我们的数据类型,数据类型必须保持一致

1.2.6 张量的各数据类型

数据类型dtypeCPU tensorGPU tensor
32位浮点型torch.float32/torch.floattorch.FloatTensortorch.cuda.FloatTensor
64位浮点型torch.float64/torch.doubletorch.DoubleTensortorch.cuda.DoubleTensor
16位浮点型torch.float16/torch.halftorch.HalfTensortorch.cuda.HalfTensor
8位无符号整型torch.unit8torch.ByteTensortorch.cuda.ByteTensor
8位有符号整型torch.int8torch.CharTensortorch.cuda.CharTensor
16位有符号整型torch.int16/torch.shorttorch.ShortTensortorch.cuda.ShortTensor
32位有符号整型torch.int32/torch.inttorch.IntTensortorch.cuda.IntTensor
64位有符号整型torch.int64/torch.longtorch.LongTensortorch.cuda.LongTensor

1.3 张量基本操作

1.3.1 维度变换

  1. 在张量做加减乘除等运算时,需要保证张量的形状一致,往往需要对某些张量进行更改

1)torch.view()

  1. 示例
x = torch.randn(4, 4)
y = x.view(16)
z = x.view(-1, 8) # -1是指这一维的维数由其他维度决定
print(x.size(), y.size(), z.size())
  • 输出
torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
  1. torch.view() 返回的新tensor与源tensor共享内存(其实是同一个tensor),更改其中的一个,另外一个也会跟着改变。view() 仅仅是改变了对这个张量的观察角度
  • 示例
x += 1
print(x)
print(y) # 也加了了1
  • 输出
tensor([[ 1.3019,  0.3762,  1.2397,  1.3998],
        [ 0.6891,  1.3651,  1.1891, -0.6744],
        [ 0.3490,  1.8377,  1.6456,  0.8403],
        [-0.8259,  2.5454,  1.2474,  0.7884]])
tensor([ 1.3019,  0.3762,  1.2397,  1.3998,  0.6891,  1.3651,  1.1891, -0.6744,
         0.3490,  1.8377,  1.6456,  0.8403, -0.8259,  2.5454,  1.2474,  0.7884])

2)torch.reshape()

  1. 可以改变张量的形状,但并不保证返回的是其拷贝值,官方不推荐使用

  2. 建议方法是:先用clone()创造一个张量副本,然后再使用torch.view()进行函数维度变换

  3. 使用 clone() 还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源 Tensor

  4. 代码示例

import torch

t1 = torch.randn((3,3)) # 构建3×3元素随机的tensor
shape = t1.shape # 返回张量的维度torch.size([3,3]),也可以通过t1.size()查看

t1_resize = torch.reshape(t1,(1,9)) # 重构成torch.size([1,9])
t1_view = t1.view(-1) #拉伸成一维张量torch.size([9]);在图像分类中,最后的分类层会完成一个处理
t1.unsqueeze = torch.unsqueeze(t1,dim=0) # 增添维度,结果为torch.size([1,3,3])
t1.squeeze = torch.squeeze(t1,dim=0) # 删减维度,结果为torch.size([3])
t1.flatten = torch.flatten() # 将两个维度间的元素进行拉伸,结果为torch.size([9])
  1. reshaperesize的区别
  • reshape会改变原有张量的类型和维度
  • resize不会
  1. flatten: 张量(c, w, h), start_dim=0, end_dim=1 ——> (cw, h)

1.3.2 拼接和拆分

  1. 在神经网络的前向传播过程中,往往需要对多个分支的张量加以融合或拆分
  2. 代码示例
import torch

t1 = torch.randn(1, 3, 256, 256) # 构建维度为[1,3,256,256]、元素随机的tensor1
t2 = torch.randn(1, 3, 256, 256) # 构建维度为[1, 3, 256, 256]、元素随机的tensor2

t_cat = torch.cat((t1,t2), dim=1) # 拼接;常用于CV
t_stack = torch.stack((t1,t2), dim=1) # 保留两个信息:原有张量的序列和矩阵;属于扩张后再拼接;常用于NLP
t_chunk = torch.chunk() # 给张量划分各种块
torch.split()

1.3.3 张量的基本计算方法

import torch
a = torch.ones(1,2)
b = torch.ones(1,2)
c = torch.randn(2,2)

add = a+b #加法运算
sub = a-b #减法运算
div = a/b #除法运算
mul = a*b #乘法运算,也可以用@或matmul
max = c.max() #求解最大元素,argmax——>即返回元素,又返回元素标识
min = c.min() #求解最小元素

1)加法运算

import torch
# 方式1
y = torch.rand(4, 3) 
print(x + y)

# 方式2
print(torch.add(x, y))

# 方式3 提供一个输出 tensor 作为参数
# 这里的 out 不需要和真实的运算结果保持维数一致,但是会有警告提示!
result = torch.empty(5, 3) 
torch.add(x, y, out=result) 
print(result)

# 方式4 in-place
y.add_(x) 
print(y)

1.3.4 其他计算

import torch
# 构建不大于输入参数的最大整数,维度为2×3
a = np.floor(10*np.random.random((2,3)))

a_ravel = a.ravel() # 把数组展平为一维数组array([x1,x2,x3,x5,x6])
a_T = a.T # 转置,不修改a本身
a_reshape_1 = a.reshape(3,2) # 把数组a_reshape_1的形状改为(3,2),不修改a本身
a_reshape_2 = a.reshape(3,-1) # -1表示将自动计算其他size,不修改a本身
a_resize_1 = a.resize(3,2) # 将修改数组a本身,没有返回值,形状改为(3,2)
a_resize_2 = np.resize(a,(3,2)) # 有返回值,不修改a本身

1.3.5 索引操作

  • 索引的结果与原数据共享内存,修改一个,另一个会跟着修改。若不想修改,可以考虑使用copy()等方法
  1. 举例
import torch
x = torch.rand(4,3)
# 取第二列
print(x[:, 1])

y = x[0,:]
y += 1
print(y)
print(x[0, :]) # 源tensor也被改了
  • 输出
tensor([-0.0720,  0.0666,  1.0336, -0.6965])

tensor([3.7311, 0.9280, 1.2497])
tensor([3.7311, 0.9280, 1.2497])

1.3.6 取值操作

  1. 如果有一个元素tensor,可以使用 .item() 来获得这个 value,而不获得其他性质
  2. 示例
import torch
x = torch.randn(1) 
print(type(x)) 
print(type(x.item()))
  • 输出
<class 'torch.Tensor'>
<class 'float'>

1.3.7 广播机制

  1. 当对两个形状不同的 Tensor 按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个 Tensor 形状相同后再按元素运算
  2. 示例
x = torch.arange(1, 3).view(1, 2)
print(x)
y = torch.arange(1, 4).view(3, 1)
print(y)
print(x + y)
  • 输出
tensor([[1, 2]])
tensor([[1],
        [2],
        [3]])
tensor([[2, 3],
        [3, 4],
        [4, 5]])
  • 由于x和y分别是1行2列和3行1列的矩阵,如果要计算x+y,那么x中第一行的2个元素被广播 (复制)到了第二行和第三行,⽽y中第⼀列的3个元素被广播(复制)到了第二列。如此,就可以对2个3行2列的矩阵按元素相加

1.4 张量自动求导

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

1.4.1 Autograd简介

  1. torch.Tensor 是这个包的核心类。如果设置它的属性 .requires_gradTrue,那么它将会追踪对于该张量的所有操作

  2. 完成计算后可以通过调用 .backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性

  • 在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的Tensor
  1. 要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪
  • 为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad():
  • 在评估模型时特别有用,因为模型可能具有 requires_grad = True 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算
  1. 还有一个类对于autograd的实现非常重要:Function
  • Tensor Function 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史
  • 每个张量都有一个.grad_fn属性,该属性引用了创建 Tensor 自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fnNone )
  • 示例(张量由用户手动创建,因此grad_fn返回结果是None)
from __future__ import print_function
import torch
x = torch.randn(3,3,requires_grad=True)
print(x.grad_fn)
None
  1. 如果需要计算导数,可以在 Tensor 上调用 .backward()
  • 如果 Tensor 是一个标量(即它包含一个元素的数据),则不需要为 backward() 指定任何参数
  • 如果它有更多的元素,则需要指定一个gradient参数,该参数是形状匹配的张量
  1. 举例(创建一个张量并设置requires_grad=True用来追踪其计算历史)
x = torch.ones(2, 2, requires_grad=True)
print(x)
  • 输出
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

对这个张量做一次运算:

y = x**2
print(y)
  • 输出
tensor([[1., 1.],
        [1., 1.]], grad_fn=<PowBackward0>)

y是计算的结果,所以它有grad_fn属性

print(y.grad_fn)
  • 输出
<PowBackward0 object at 0x000001CB45988C70>

对 y 进行更多操作

z = y * y * 3
out = z.mean()

print(z, out)
  • 输出
tensor([[3., 3.],
        [3., 3.]], grad_fn=<MulBackward0>) tensor(3., grad_fn=<MeanBackward0>)

.requires_grad_(...) 原地改变了现有张量的requires_grad标志。如果没有指定的话,默认输入的这个标志是 False

a = torch.randn(2, 2) # 缺失情况下默认requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)
  • 输出
False
True
<SumBackward0 object at 0x000001CB4A19FB50>

1.4.2 梯度

  1. 现在开始进行反向传播,因为 out 是一个标量,因此out.backward() out.backward(torch.tensor(1.)) 等价

  2. out.backward()
    

​ 输出导数 d(out)/dx

print(x.grad)
  • 输出
tensor([[3., 3.],
        [3., 3.]])
  1. 数学上,若有向量函数y=f(x)\vec{y}=f(\vec{x}),那么y\vec{y}关于x\vec{x}的梯度就是一个雅可比矩阵: J=(y1x1y1xnymx1ymxn)J=\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right)

    torch.autograd 这个包就是用来计算一些雅可比矩阵的乘积的

  2. 如果 vv 是一个标量函数 l=g(y)l = g(\vec{y}) 的梯度: v=(ly1lym)v=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right) 由链式法则,我们可以得到: vJ=(ly1lym)(y1x1y1xnymx1ymxn)=(lx1lxn)v J=\left(\begin{array}{lll}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right)\left(\begin{array}{ccc}\frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}}\end{array}\right)=\left(\begin{array}{lll}\frac{\partial l}{\partial x_{1}} & \cdots & \frac{\partial l}{\partial x_{n}}\end{array}\right)

  • grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零
# 再来反向传播⼀一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)

out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)
  • 输出
tensor([[4., 4.],
        [4., 4.]])
tensor([[1., 1.],
        [1., 1.]])

现在我们来看一个雅可比向量积的例子:

x = torch.randn(3, requires_grad=True)
print(x)

y = x * 2
i = 0
while y.data.norm() < 1000:
    y = y * 2
    i = i + 1
print(y)
print(i)
  • 输出
tensor([-0.9332, 1.9616, 0.1739], requires_grad=True)
tensor([-477.7843, 1004.3264, 89.0424], grad_fn=<MulBackward0>)
8

在这种情况下,y 不再是标量。torch.autograd 不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给 backward

v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)

print(x.grad)
  • 输出
tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])
  1. 也可以通过将代码块包装在 with torch.no_grad(): 中,来阻止 autograd 跟踪设置了.requires_grad=True的张量的历史记录
print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
    print((x ** 2).requires_grad)
  • 输出
True
True
False
  1. 如果想要修改 tensor 的数值,但是又不希望被 autograd 记录(即不会影响反向传播), 那么我们可以对 tensor.data 进行操作。
x = torch.ones(1,requires_grad=True)

print(x.data) # 还是一个tensor
print(x.data.requires_grad) # 但是已经是独立于计算图之外

y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播

y.backward()
print(x) # 更改data的值也会影响tensor的值 
print(x.grad)
  • 输出
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])

1.5 并行计算

  1. 当服务器上有多个GPU,应该指明使用的GPU是哪一块.如果不设置,tensor.cuda()方法会默认将 tensor 保存到第一块GPU上,等价于tensor.cuda(0),这将会导致爆出out of memory的错误
  2. 可以通过以下两种方式继续设置:
  • 方法一
#设置在文件最开始部分
import os
os.environ["CUDA_VISIBLE_DEVICE"] = "2" # 设置默认的显卡
  • 方法二
CUDA_VISBLE_DEVICE=0,1 python train.py # 使用0,1两块GPU

1.5.1 网络结构分布到不同的设备(Network partitioning)

  1. 思路:将一个模型的各个部分拆分,然后将不同的部分放入到GPU来做不同任务的计算。其架构如下: 在这里插入图片描述
  2. 问题:不同模型组件在不同的GPU上时,GPU间的传输就很重要,对于GPU间的通信是一个考验。但是GPU的通信在这种密集任务中很难办到,所以这个方式慢慢淡出了视野

1.5.2 同一层的任务分布到不同的数据中(Layer-wise partitioning)

  1. 思路:同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。其架构如下: 在这里插入图片描述
  2. 问题:在需要大量训练,同步任务加重的情况下,会出现和第一种方式一样的问题

1.5.3 不同的数据分布到不同的设备中,执行相同的任务(Data parallelism)

  1. 思路:不再拆分模型,训练时模型都是一整个模型。但是将输入的数据拆分。所谓的拆分数据就是,同一个模型在不同GPU中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传。其架构如下: 在这里插入图片描述
  2. 这种方式可以解决之前模式遇到的通讯问题。现在的主流方式是数据并行的方式(Data parallelism)

1.6 Numpy相关基础

1.6.1 Numpy构建的数据

import numpy as np

# 构建numpy数据
a = np.array([[1,2,3],[4,5,6]])
# 数组类别:class numpy.ndarray
print(type(a))
# 数据类型:float64
print(a.dtype)
# 数组形状(2×3)
print(a.shape)
# 数组元素个数6
print(a.size)

1.6.2 Numpy中常用的生成指令

import numpy as np

# 以0为起点,间隔为1,7为终点生成一维数组
a = np.arrange(0,7,1,dtype=np.int16)
# 构建维度为2×3,元素全为1的整型矩阵
a = np.ones((2,3),dtype=np.int16)
# 构建维度为2×3,元素全为0的矩阵
a = np.zeros((2,3))
# 构建维度为2×3的空矩阵数组
a = np.empty((2,3))
# 在[-1,2]区间内取5个点构成数组
a = np.linspace(-1,2,5)
# 构建维度为2×3的数组,各元素大于等于0,小于10
a = np.random.randint(0,10,(2,3))

1.6.3 常用的运算指令

import numpy as np

a = np.ones(1,2)
b = np.ones(1,2)

add = a+b # 加法运算
sub = a-b # 减法运算
div = a/b # 除法运算
mul = a*b # 乘法运算,也可以用@或matmul

1.6.4 形状查看及更改

import numpy as np

# 构建不大于输入参数的最大整数,维度为2×3
a = np.floor(10*np.random.random((2,3)))

a_ravel = a.ravel() # 把数组展平为一维数组array([x1,x2,x3,x4,x5,x6])
a_T = a.T # 转置,不修改a本身
a_reshape_1 = a.reshape(3,2) # 把数组a_reshape_1的形状改为(3,2),不修改a本身
a_reshape_2 = a.reshape(3,-1) # -1表示将自动计算其他size,不修改a本身
a_resize_1 = a.resize(3,2) # 将修改数组a本身,没有返回值,形状改为(3,2)
a_resize_2 = np.resize(a,(3,2)) # 有返回值,不修改a本身

1.6.5 PyTorch和Numpy对比

对比项NumpyPyTorch
相同点可以定义多维数组,进行切片、改变维度、数学运算等可以定义多维数组,进行切片、改变维度、数学运算等
不同点1. 产生的数组类型为numpy.ndarray
2. 会将ndarray放入CPU运算
3. 导入方式为import numpy
4. numpy中没有data.type()的用法,只可以使用type(data)
1. 产生的数组类型为torch.Tensor
2. 可以将tensor放入CPU或GPU运算
3. 导入方式为import torch
4. Pytorch中既可以用data.type(),也可以用type(data)
5. tensor含有更加复杂的设置参数,如device(是否利用GPU)、requires_grad(是否需要求导)等

1.7 PyTorch的API汇总

1.torch.nn: 神经网络库,包括网络层与损失函数
2.torch.nn.functional: 神经网络基本函数
3.torch.nn.init: 初始化函数
4.torch.Tensor: 基本数据结构
5.Tensor Attributes
6.Tensor Views
7.torch.autograd: 构建计算图并自动计算梯度
8.torch.cuda: cuda包
9.torch.cuda.amp
10.torch.backends
11.torch.distributed
12.torch.distributions
13.torch.fft
14.torch.futures
15.torch.hub
16.torch.linalg
17.torch.onnx: 
18.torch.optim: 
19.Complex Numbers
20.Quantization
21.Distributed RPC Framework
22.torch.random
23.torch.sparse
24.torch.Storage
25.torch.utils.bottleneck
26.torch.utils.checkpoint
27.torch.utils.cpp extension
28.torch.utils.data: 
29.torch.utils.dlpack
30.torch.utils.mobile_optimizer
31.torch.utils.model_zoo
32.torch.utils.tensorboard
33.Type Info
34.Named Tensors
35.Named Tensors operator
   coverage
36.torch._config_

资料参考来源:1. Datawhale社区《深入浅出PyTorch教程》 2. 有三AI《PyTorch入门及实战》 3. 其他零散网络资源