PyTorch是一个开源的深度学习框架,由Facebook的AI研究团队于2016年发布。它提供了丰富的工具和库,用于构建、训练和部署深度神经网络模型。PyTorch采用动态计算图的设计,使得构建和调试模型变得直观而灵活。它的自动求导功能也使得实现反向传播算法变得简单,是训练深度学习模型的关键组件。接下来,让我们一起了解一下Pytorch的基础语法,为后续Pytorch实战打好基础,本节主要分为如下两个主要内容:
- 创建张量:学会如何创建空张量、随机张量、全零张量以及使用已有数据创建张量。
- 张量的操作:学会如何对张量进行操作,包括张量的加减乘除运算、shape变换、转置、索引与切片以及数组、列表与张量之间的互转。
- 自动求导原理与实现:自动求导是一个关键的功能,它允许我们自动计算梯度,从而进行反向传播和优化。
- 反向传播:反向传播是深度学习中的关键概念之一,它是训练神经网络模型的基础。
一、张量的概念和表示
在PyTorch中,张量(tensor)是最基本的数据结构,它类似于多维数组。张量在深度学习中扮演着核心的角色,用于表示和处理数据以及进行数值计算。
1. 张量的维度和形状
张量可以具有不同的维度和形状。在PyTorch中,我们可以使用torch.Tensor类来创建张量。以下是一些常见的张量及其对应的维度和形状的示例:
- 标量(0维张量):一个单独的数值。例如,
tensor = torch.tensor(5)表示一个标量,其维度为0,形状为空。 - 向量(1维张量):具有一个轴的张量。例如,
tensor = torch.tensor([1, 2, 3, 4, 5])表示一个向量,其维度为1,形状为(5,)。 - 矩阵(2维张量):具有两个轴的张量。例如,
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])表示一个矩阵,其维度为2,形状为(2, 3)。 - 3维张量:具有三个轴的张量。例如,
tensor = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])表示一个3维张量,其维度为3,形状为(2, 2, 2)。
您可以使用torch.Tensor类的构造函数以及其他一些创建张量的函数来创建具有不同维度和形状的张量。下面使用代码进行介绍1
2
3
4
5
6
7
8
9# 导入PyTorch库
import torch
# 创建一个PyTorch张量(tensor),包含数字1到5
tensor = torch.tensor([1, 2, 3, 4, 5])
# 打印张量的形状(shape),即张量中元素的维度信息
print(tensor.shape)
# torch.Size([5])
2. 张量的操作
PyTorch提供了许多用于操作张量的函数和方法。以下是一些常用的张量操作:
- 创建张量:可以使用
torch.tensor函数从Python列表或NumPy数组创建张量,也可以使用其他创建函数,如torch.zeros、torch.ones、torch.rand等创建具有特定形状和初始值的张量。
1 | # 创建一个形状为(2, 3)的零张量 |
张量操作:可以对张量执行各种数学运算和操作,如加法、减法、乘法、除法、矩阵乘法等。这些操作可以使用算术运算符或对应的函数来执行。
1
2
3
4
5
6
7
8
9
10tensor1 = torch.tensor([1, 2, 3])
tensor2 = torch.tensor([4, 5, 6])
# 加法操作
result = tensor1 + tensor2
print(result) # 输出: tensor([5, 7, 9])
# 乘法操作
result = tensor1 * tensor2
print(result) # 输出: tensor([4, 10, 18])张量索引和切片:可以使用索引和切片操作来访问张量中的特定元素或子张量。
1 | import torch |
3. 张量的GPU加速
在PyTorch中,可以将张量存储在CPU或GPU上。GPU加速可以显著提高深度学习模型的计算性能。要将张量移动到GPU上,可以使用.to方法。
当然我们也可以在创建张量时,通过 .cuda(), 或 device 参数直接指定将张量存储在GPU上
1 | import torch |
如果您的系统具有可用的GPU,并且已正确配置PyTorch以使用GPU加速,那么上述代码将使张量存储在GPU上。否则,它将继续在CPU上运行。
下面以计算三个矩阵相乘的结果为例,我们分别通过 CPU 和 NVIDIA Tesla V100 GPU 来直观感受一下使用GPU的带来的速度提升。1
2
3
4
5
6
7
8import torch
import timeit
M = torch.rand(1000, 1000)
print(timeit.timeit(lambda: M.mm(M).mm(M), number=5000))
N = torch.rand(1000, 1000).cuda()
print(timeit.timeit(lambda: N.mm(N).mm(N), number=5000))
4. 将张量转移到CPU
1 | tensor = tensor.to('cpu') |
这是关于”张量的概念和表示”部分的一个简要介绍。在接下来的教程中,我们将深入探讨更多有关PyTorch的功能和操作。
二、创建张量
在PyTorch中,张量(Tensor)是一种多维数组,类似于NumPy的ndarray对象。张量可以用来表示数据和进行各种数学运算。本节将介绍如何创建不同类型的张量。
1. 创建空张量
要创建一个空张量,可以使用torch.empty()函数。空张量的元素值将不会被初始化,它们的内容是未知的。1
2# 创建一个未初始化的 5x3 张量
x = torch.empty(5, 3)
2. 创建随机张量
要创建一个随机初始化的张量,可以使用torch.rand()函数。这将生成一个在[0, 1)范围内均匀分布的随机张量。1
2# 创建一个形状为 2x2 的随机张量
x = torch.rand(2, 2)
3. 创建全零张量
要创建一个全零的张量,可以使用torch.zeros()函数。1
2# 创建一个形状为 3x3 的全零张量
x = torch.zeros(3, 3)
4. 创建全1张量
1 | # 创建一个形状为 3x3 的全零张量 |
5. 从数据创建张量
你还可以从现有的数据创建张量。可以使用torch.tensor()函数从Python列表或NumPy数组创建张量。1
2
3
4
5
6
7
8
9
10# 从Python列表创建张量
data_list = [1, 2, 3, 4, 5]
x = torch.tensor(data_list)
print(x)
# 从NumPy数组创建张量
data_array = np.array([6, 7, 8, 9, 10])
x = torch.from_numpy(data_array) # from_numpy 方法
x = torch.tensor(data_array) # 张量转换
print(x)
6. 创建具有特定数据类型的张量
在PyTorch中,张量可以具有不同的数据类型,如浮点型、整型等。可以使用dtype参数指定张量的数据类型。1
2
3
4
5
6# 创建一个具有特定数据类型的张量
x = torch.tensor([1, 2, 3], dtype=torch.float32)
print(x.dtype)
# 创建一个具有特定数据类型的全零张量
x = torch.zeros(3, 3, dtype=torch.int32)
三、张量操作和运算
1. 张量运算
PyTorch提供了许多张量运算操作,例如加法、减法、乘法和除法。这些运算可以应用于张量之间,也可以应用于张量与标量之间。下面是一些常用的张量运算示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30# 张量加法
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
c = a + b
# 张量减法
c = a - b
# 张量乘法
c = a * b
# 张量除法
c = a / b
# 张量与标量相加
c = 2
d = a + c
# 张量与标量相乘
d = a * c
# 计算点积
a.dot(b)
tensor(32., dtype=torch.float64)
#
a.sin()
tensor([0.8415, 0.9093, 0.1411], dtype=torch.float64)
a.exp()
tensor([ 2.7183, 7.3891, 20.0855], dtype=torch.float64)
指定维度的张量运算,对张量进行聚合(如求平均、求和、最大值和最小值等)或拼接操作时,可以指定进行操作的维度 (dim)。1
2
3
4
5
6
7
8
9
10a = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.double)
# 计算张量的平均值,在默认情况下会计算所有元素的平均值。:
a.mean()
# 设定计算的维度,可以实现分别对第 0 维和第 1 维计算平均值:
x.mean(dim=0) # tensor([2.5000, 3.5000, 4.5000], dtype=torch.float64)
x.mean(dim=1) # tensor([2., 5.], dtype=torch.float64)
# 注意,上面的计算自动去除了多余的维度,因此结果从矩阵变成了向量,如果要保持维度不变,可以设置 keepdim=True:
x.mean(dim=0, keepdim=True) # tensor([[2.5000, 3.5000, 4.5000]], dtype=torch.float64)
拼接 cat
拼接 torch.cat 操作类似,通过指定拼接维度,可以获得不同的拼接结果:1
2
3
4
5
6
7
8
9
10x = torch.tensor([[1, 2, 3], [ 4, 5, 6]], dtype=torch.double)
y = torch.tensor([[7, 8, 9], [10, 11, 12]], dtype=torch.double)
torch.cat((x, y), dim=0)
tensor([[ 1., 2., 3.],
[ 4., 5., 6.],
[ 7., 8., 9.],
[10., 11., 12.]], dtype=torch.float64)
torch.cat((x, y), dim=1)
tensor([[ 1., 2., 3., 7., 8., 9.],
[ 4., 5., 6., 10., 11., 12.]], dtype=torch.float64)
2. 张量形状变换
在实际的深度学习任务中,经常需要对张量进行形状变换。PyTorch提供了一系列函数来改变张量的形状,例如reshape()、view()和transpose()等。下面是一些常用的张量形状变换的示例:
形状转换
view, 将张量转换为新的形状,需要保证总的元素个数不变。
进行view操作的张量必须是连续的 (contiguous),可以调用is_conuous来判断张量是否连续;如果非连续,需要先通过contiguous函数将其变为连续的。也可以直接调用Pytorch新提供的reshape函数,它与view功能几乎一致,并且能够自动处理非连续张量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17x = torch.tensor([1, 2, 3, 4, 5, 6])
print(x, x.shape)
tensor([1, 2, 3, 4, 5, 6]) torch.Size([6])
x.view(2, 3) # shape adjusted to (2, 3)
tensor([[1, 2, 3],
[4, 5, 6]])
x.view(3, 2) # shape adjusted to (3, 2)
tensor([[1, 2],
[3, 4],
[5, 6]])
x.view(-1, 3) # -1 means automatic inference
tensor([[1, 2, 3],
[4, 5, 6]])
x.reshape(3, 2) # 改变为3x2的张量
tensor([[1, 2, 3],
[4, 5, 6]])转置
transpose, 交换张量中的两个维度,参数为相应的维度:1
2
3
4
5
6
7
8x = torch.tensor([[1, 2, 3], [4, 5, 6]])
x
tensor([[1, 2, 3],
[4, 5, 6]])
x.transpose(0, 1)
tensor([[1, 4],
[2, 5],
[3, 6]])交换维度
permute与transpose函数每次只能交换两个维度不同,permute可以直接设置新的维度排列方式:1
2
3
4
5
6
7
8
9
10
11x = torch.tensor([[[1, 2, 3], [4, 5, 6]]])
print(x, x.shape)
tensor([[[1, 2, 3],
[4, 5, 6]]]) torch.Size([1, 2, 3])
x = x.permute(2, 0, 1)
print(x, x.shape)
tensor([[[1, 4]],
[[2, 5]],
[[3, 6]]]) torch.Size([3, 1, 2])
3. 降维与升维
有时为了计算需要对一个张量进行降维或升维。例如神经网络通常只接受一个批次 (batch) 的样例作为输入,如果只有 1 个输入样例,就需要手工添加一个 batch 维度。具体地:
- 升维 torch.unsqueeze(input, dim, out=None) 在输入张量的 dim 位置插入一维,与索引一样,dim 值也可以为负数;
降维 torch.squeeze(input, dim=None, out=None) 在不指定 dim 时,张量中所有形状为 1 的维度都会被删除,例如$[A,1,B,1,C]$ 会变成 $[A,B,C]$; 当给定 dim 时,只会删除给定的维度(形状必须为 1),例如 $[A,1,B]$ squeeze(input, dim=0) 会保持张量不变,只有 squeeze(input, dim=1) 形状才会变成 $[A,B]$.
。
下面是一些示例:1
2
3
4
5
6
7
8
9
10
11
12a = torch.tensor([1, 2, 3, 4])
a.shape
torch.Size([4])
b = torch.unsqueeze(a, dim=0)
print(b, b.shape)
tensor([[1, 2, 3, 4]]) torch.Size([1, 4])
b = a.unsqueeze(dim=0) # another way to unsqueeze tensor
print(b, b.shape)
tensor([[1, 2, 3, 4]]) torch.Size([1, 4])
c = b.squeeze()
print(c, c.shape)
tensor([1, 2, 3, 4]) torch.Size([4])
3. 张量索引和切片
对于张量中的元素,可以使用索引和切片来访问和操作它们。下面是一些常用的张量索引和切片的示例:1
2
3
4# 张量索引
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
element = a[0, 1] # 获取第一行第二列的元素
slice = a[:, 1:3] # 获取所有行的第二列到第三列的切片
4. 获取张量中的数值
如果
tensor仅有一个元素,可以采用.item()来获取数值:1
2
3
4a = torch.randn(1)
print(a)
print(a.item())如果张量中含有多个元素
1
2
3
4a = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(a[1:2,1])
print(a[1:2,1].item())
5. 张量转化为数组
1 | # 创建一个PyTorch张量 |
6. 张量转化为列表
1 | # 创建一个PyTorch张量 |
请注意,.tolist()方法只能用于包含标量元素的张量,例如整数或浮点数。如果张量包含其他张量或复杂数据类型,则不能直接使用.tolist()。在这种情况下,你需要先将内部的张量或复杂数据类型转换为Python列表。
四、自动求导原理
1. 梯度
在深度学习中,梯度是一个非常重要的概念,它表示了函数在某一点的变化率。在PyTorch中,计算梯度是一项关键操作,它允许我们通过反向传播算法有效地更新模型参数。本节将详细介绍如何在PyTorch中计算梯度,并提供一些示例来帮助你更好地理解这个过程。
梯度是一个向量,其方向指向函数值增长最快的方向,而其大小表示函数值的变化率。在深度学习中,我们通常希望最小化损失函数,因此我们要沿着梯度的反方向更新模型参数,以逐步降低损失值。
PyTorch中的torch.Tensor类是PyTorch的核心数据结构,同时也是计算梯度的关键。每个张量都有一个属性requires_grad,默认为False。如果我们希望计算某个张量的梯度,需要将requires_grad设置为True,那么就会开始追踪在该变量上的所有操作,而完成计算后,可以调用 .backward() 并自动计算所有的梯度,得到的梯度都保存在属性 .grad 中。
调用 .detach() 方法分离出计算的历史,可以停止一个 tensor 变量继续追踪其历史信息 ,同时也防止未来的计算会被追踪。而如果是希望防止跟踪历史(以及使用内存),可以将代码块放在 with torch.no_grad(): 内,这个做法在对模型进行评估的时候非常有用(节约算力、不会发生模型参数变化)。
对于 autograd 的实现,还有一个类也是非常重要–Function 。Tensor 和 Function 两个类是有关联并建立了一个非循环的图,可以编码一个完整的计算记录。每个 tensor 变量都带有属性 .grad_fn ,该属性引用了创建了这个变量的 Function (除了由用户创建的 Tensors,它们的 grad_fn二小节介绍梯度的内容。
在PyTorch中,自动求导是一个关键的功能,它允许我们自动计算梯度,从而进行反向传播和优化。
2. 梯度计算过程
在PyTorch中,计算梯度的过程主要分为以下几个步骤:
1 | # 1. 创建张量并设置`requires_grad=True`: |
在上面的示例中,我们创建了一个包含两个元素的张量x,并将requires_grad设置为True。使用backward()方法计算梯度,并通过访问x.grad获取了梯度值。
问题一:为什么要先执行sum再求导?
上面的代码中,先执行y.sum()再求导的目的是为了计算一个标量值(scalar)的梯度。PyTorch的自动微分机制是基于标量值的,因此我们需要确保我们要计算的梯度是一个标量。具体解释如下:
y是一个张量,它可能包含多个元素,例如[y1, y2, ...],每个元素都与x相关。如果我们直接调用y.backward(),PyTorch会尝试计算y中每个元素对应的梯度,这将得到一个与x具有相同形状的梯度张量。这在某些情况下可能是有用的,但通常我们更关心的是一个标量目标函数的梯度。- 通过执行
y.sum(),我们将y中的所有元素相加,得到一个标量值(单个数字)。然后,我们对这个标量值执行反向传播,计算相对于x的梯度。这确保了我们计算的是整个目标函数相对于x的梯度,而不是每个元素的梯度。
如果去掉上面代码中的.sum(),将会提示你
RuntimeError: grad can be implicitly created only for scalar outputs
3. 梯度计算示例
下面是一些更复杂的示例,以帮助你更好地理解计算梯度的过程。
示例 1:线性回归
考虑一个简单的线性回归模型,我们的目标是找到一条直线,以最小化预测值与真实值之间的平方误差。我们可以使用梯度下降算法来更新直线的参数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37import torch
# 创建训练数据
x_train = torch.tensor([[1.0], [2.0], [3.0]])
y_train = torch.tensor([[2.0], [4.0], [6.0]])
# 定义模型参数
w = torch.tensor([[0.0]], requires_grad=True)
b = torch.tensor([[0.0]], requires_grad=True)
# 定义模型
def linear_regression(x):
return torch.matmul(x, w) + b
# 定义损失函数
def loss_fn(y_pred, y):
return torch.mean((y_pred - y)**2)
# 定义优化器
optimizer = torch.optim.SGD([w, b], lr=0.01)
# 训练模型
for epoch in range(100):
# 前向传播
y_pred = linear_regression(x_train)
# 计算损失
loss = loss_fn(y_pred, y_train)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 清零梯度
optimizer.zero_grad()
五、反向传播
1. 反向传播原理
反向传播(Backpropagation,缩写为BP)是“误差反向传播”的简称,该方法对网络中所有权重计算损失函数的梯度。 这个梯度会反馈给梯度下降法,用来更新权重值以最小化损失函数,从而训练神经网络模型。
而反向传播是计算损失函数对模型参数梯度的一种有效方法。通过计算参数梯度,我们可以根据梯度的反方向更新参数,使得模型的预测结果逐渐接近真实标签。反向传播的过程可以概括为以下几个步骤:
- 前向传播:将输入样本通过神经网络的前向计算过程,计算出预测结果。
- 计算损失:将预测结果与真实标签进行比较,并计算损失函数的值。
- 反向传播梯度:根据损失函数的值,计算损失函数对模型参数的梯度。
- 参数更新:根据参数的梯度和优化算法的规则,更新模型的参数。
在PyTorch中,反向传播过程是自动完成的。通过调用backward()函数,PyTorch会自动计算损失函数对所有可学习参数的梯度,并将其保存在相应的参数张量的.grad属性中。接下来,我们将通过具体示例来演示这一过程。
2. 反向传播示例
假设我们有一个简单的线性回归模型,其中只有一个输入特征和一个输出标签。我们的目标是通过训练模型来找到最佳的线性关系。
首先,我们需要导入所需的库和模块、定义模型类1
2
3
4
5
6
7
8
9
10
11import torch
import torch.nn as nn
import torch.optim as optim
class LinearRegression(nn.Module):
def __init__(self):
super(LinearRegression, self).__init__()
self.linear = nn.Linear(1, 1) # 输入维度为1,输出维度为1
def forward(self, x):
return self.linear(x)
然后,我们创建模型的实例、定义损失函数和优化器:
1 | model = LinearRegression() |
现在,我们生成一些样本数据,并进行训练:
1 | # 生成样本数据 |
在上述代码中,我们使用了一个简单的数据集,包含了输入特征x_train和对应的真实标签y_train。在每个训练迭代中,我们首先将梯度清零,然后进行前向传播计算预测值y_pred,接着计算损失loss,然后通过调用.backward()函数执行反向传播,最后使用优化器的.step()方法来更新模型的参数。
通过上述示例,我们可以看到在PyTorch中实现反向传播是非常简单的。PyTorch会自动计算参数的梯度,并通过优化器来更新参数,从而实现模型的训练过程。