2.0 为什么需要学习 PyTorch

学习 PyTorch 的核心意义在于,它是目前最主流、最灵活的深度学习框架之一,已经成为学术研究和工业实践的“事实标准”。PyTorch 以张量(Tensor)为核心数据结构,结合动态计算图机制(Autograd),让模型的构建、调试与梯度计算都像写普通 Python 一样自然直观。它支持 GPU 加速计算,集成了丰富的神经网络模块(如 torch.nntorch.optimtorchvision 等),几乎涵盖了从图像识别、自然语言处理到推荐系统等所有深度学习领域。

更重要的是,PyTorch 拥有庞大而活跃的开源生态,是许多前沿研究论文、竞赛解决方案以及工业级 AI 系统的首选框架。掌握 PyTorch,不仅能深入理解深度学习模型的底层原理(如前向传播、反向传播、梯度下降),还能快速将想法转化为可运行的实验代码,为科研创新和实际工程打下坚实基础。

PyTorch 的安装可以参照:[安装 PyTorch](Get Started) 。(下面代码均在 PyTorch 2.4.0 + CUDA 12.4 版本下完成,注意 PyTorch 安装时分为 CPU 和 GPU 两个版本,可以根据自己的电脑配置进行选择)

2.1 PyTorch 安装测试

import torch

# <====== 2.1 PyTorch 安装测试 =====>
print(f'PyTorch 版本: {torch.__version__}')
print(f'是否支持 CUDA: {torch.cuda.is_available()}')
if torch.cuda.is_available():
print(f'CUDA 版本: {torch.version.cuda}')
print(f'cuDNN 版本: {torch.backends.cudnn.version()}')
print(f'设备数量: {torch.cuda.device_count()}')
for i in range(torch.cuda.device_count()):
print(f' 设备 {i}: {torch.cuda.get_device_name(i)}')
print(f' 显存: {torch.cuda.get_device_properties(i).total_memory / 1024 ** 3:.2f} GB')
else:
print('⚠️ 当前环境为 CPU 版本,无可用 CUDA 设备。')

# PyTorch 主要由 5 个核心组件构成:
# 1) torch.Tensor(tensor): 多维数组,类似于 NumPy 的 ndarray,但增加了对 GPU 加速的支持
# 2) torch.autograd: 自动获取梯度,支持动态计算图,方便神经网络的训练
# 3) torch.optim: 优化器模块,提供了多种优化算法,如 SGD、Adam 等
# 4) torch.nn: 神经网络模块,提供了构建神经网络的各种层和损失函数
# 5) torch.utils.data: 数据处理模块,提供了数据加载和预处理的工具
# 第二章主要学习 Tensor、autograd 和 optim 三个组件,第三章会学习 nn 组件,第四章则会学习 utils.data 组件


def main():
x = torch.tensor([10.0])
if torch.cuda.is_available():
x = x.cuda() # 迁移到 GPU
print(x)

y = torch.randn(2, 3)
if torch.cuda.is_available():
y = y.cuda() # 迁移到 GPU
print(y)

z = x + y
print(z)


if __name__ == '__main__':
main()

2.2 NumPy 与 PyTorch

import torch
import numpy as np

# <====== 2.2 NumPy 与 PyTorch =====>
# <----- 2.2.1 Tensor 的创建 ------>
# Tensor 是 PyTorch 中的核心数据结构,类似于 NumPy 中的 ndarray。Tensor 可以在 GPU 上运行,从而加速计算。
# Tensor 支持多种数据类型,包括浮点型、整型、布尔型等。Tensor 的维度可以是任意的,常见的有一维向量、二维矩阵和三维张量等。
# Tensor 的创建方式有多种,可以从列表、NumPy 数组等创建,也可以使用 PyTorch 提供的函数创建。
# 1) 从列表创建 Tensor
print('*' * 10 + ' 从列表创建 Tensor ' + '*' * 10)
data_list = [[1, 2], [3, 4]]
data_tensor1 = torch.tensor(data_list)
print(f'从列表创建的 Tensor:\n{data_tensor1}, 类型:{type(data_tensor1)}')
# 注意:从列表创建的 Tensor 与原始列表不共享内存,修改其中一个不会影响另一个
data_tensor1[0] = torch.tensor([5, 6]) # 修改 Tensor 的值
print(f'修改后的 Tensor:\n{data_tensor1},\n原始列表不变:\n{data_list}')

# 2) 从 NumPy 数组创建 Tensor
print('*' * 10 + ' 从 NumPy 数组创建 Tensor ' + '*' * 10)
data_ndarray = np.array([[3, 4], [5, 6]])
data_tensor2 = torch.tensor(data_ndarray)
# 也可以使用 torch.from_numpy() 函数创建 Tensor(推荐)
data_tensor3 = torch.from_numpy(data_ndarray)
print(f'从 NumPy 数组创建的 Tensor:\n{data_tensor2}, 类型:{type(data_tensor2)}')
print(f'使用 from_numpy 创建的 Tensor:\n{data_tensor3}, 类型:{type(data_tensor3)}')
# 注意:从 NumPy 数组创建的 Tensor 与原始数组共享内存,修改其中一个会影响另一个
data_tensor2[0][1] = 5 # 修改 Tensor 的值
print(f'修改后的 Tensor:\n{data_tensor2},\n原始 NumPy 数组也变了:\n{data_ndarray}')

# 3) 使用 PyTorch 提供的函数创建 Tensor
# 常见的函数有 Tensor(*size)、eye(row, column)、linspace(start, end, steps)、logspace(start, end, steps)、torch.arange(start, end, step)
# zeros(*size)、ones(*size)、rand(*size)、randn(*size)、ones_like(t)、zeros_like(t)、from_numpy(ndarray)等
print('*' * 10 + ' 使用 PyTorch 提供的函数创建 Tensor ' + '*' * 10)
# 根据指定形状创建 Tensor
data_tensor4 = torch.Tensor(2, 3, 4) # 创建一个未初始化的 2x3x4 Tensor
print(f'未初始化的 Tensor:\n{data_tensor4}, 形状:{data_tensor4.shape}') # Tensor.shape 和 Tensor.size() 返回 Tensor 的形状
data_tensor5 = torch.empty(2, 3, 4) # 创建一个未初始化的 2x3x4 Tensor
print(f'未初始化的 Tensor:\n{data_tensor5}, 形状:{data_tensor5.size()}')
# 创建单位矩阵
data_tensor6 = torch.eye(3, 4) # 创建一个 3x4 的单位矩阵
print(f'单位矩阵:\n{data_tensor6}, 形状:{data_tensor6.size()}')
# 创建等差数列
data_tensor7 = torch.linspace(1, 10, steps=5) # 创建一个从 1 到 10 的等差数列,包含 5 个元素
print(f'等差数列:\n{data_tensor7}, 形状:{data_tensor7.size()}')
# 创建等比数列
data_tensor8 = torch.logspace(1, 10, steps=5) # 创建一个从 10^1 到 10^10 的等比数列,包含 5 个元素
print(f'等比数列:\n{data_tensor8}, 形状:{data_tensor8.size()}')
# 创建指定范围的数列
data_tensor9 = torch.arange(1, 10, step=2) # 创建一个从 1 到 10,步长为 2 的数列
print(f'指定范围的数列:\n{data_tensor9}, 形状:{data_tensor9.size()}')
# 创建全零或全一的 Tensor
data_tensor10 = torch.zeros(2, 3) # 创建一个 2x3 的全零 Tensor
print(f'全零 Tensor:\n{data_tensor10}, 形状:{data_tensor10.size()}')
data_tensor11 = torch.ones(2, 3) # 创建一个 2x3 的全一 Tensor
print(f'全一 Tensor:\n{data_tensor11}, 形状:{data_tensor11.size()}')
# 创建随机数 Tensor
data_tensor12 = torch.rand(2, 3) # 创建一个 2x3 的[0, 1)均匀分布随机数 Tensor
print(f'均匀分布随机数 Tensor:\n{data_tensor12}, 形状:{data_tensor12.size()}')
data_tensor13 = torch.randn(2, 3) # 创建一个 2x3 的标准正态分布随机数 Tensor
print(f'标准正态分布随机数 Tensor:\n{data_tensor13}, 形状:{data_tensor13.size()}')
# 根据已有 Tensor 创建新的 Tensor
data_tensor14 = torch.ones_like(data_tensor13) # 创建一个与 data_tensor13 形状相同的全一 Tensor
print(f'与已有 Tensor 形状相同的全一 Tensor:\n{data_tensor14}, 形状:{data_tensor14.size()}')

# 注意 Tensor 和 tensor 的区别
# 1) torch.Tensor 是 torch.tensor 和 torch.empty 的一种混合
# 当传入数据时,torch.Tensor 使用全局默认数据类型(FloatTensor),而 torch.tensor 则会根据数据类型自动推断
# torch.Tensor(1) 创建一个标量 Tensor,数据类型为 torch.float32,随机初始化值; torch.tensor(1) 创建一个标量 Tensor,数据类型为 torch.int64,值为 1
t1 = torch.tensor(1)
t2 = torch.Tensor(1)
print(f'torch.tensor(1): {t1}, dtype: {t1.dtype}, shape: {t1.shape}')
print(f'torch.Tensor(1): {t2}, dtype: {t2.dtype}, shape: {t2.shape}')
t3 = torch.tensor([1, 2, 3]) # 根据数据类型自动推断为 torch.int64
t4 = torch.Tensor([1, 2, 3]) # 默认数据类型为 torch.float32
print(f'torch.tensor() 推断数据类型: {t3.dtype}, {t3}')
print(f'torch.Tensor() 默认数据类型为 Float: dtype: {t4.dtype}, {t4}')

# <----- 2.2.2 Tensor 的概述 ------>
# Tensor 的操作从接口来看可以分成两类: torch.function() 和 tensor.function()。前者是静态方法,后者是实例方法。
# 这两类方法大部分功能是相同的,区别在于前者需要将 Tensor 作为第一个参数传入,而后者则是直接调用实例方法。
x = torch.eye(3, 3)
y = torch.ones(3, 3)
print(f'张量 x:\n{x}\n张量 y:\n{y}')
print(f'静态方法 x + y:\n{torch.add(x, y)}') # 使用静态方法进行加法
print(f'实例方法 x + y:\n{x.add(y)}') # 使用实例方法进行加法
# 从修改的角度来看,可以分成两类: 不修改原始 Tensor 的操作和修改原始 Tensor 的操作。
# 不修改自身的数据,比如 x.add(y) 后 x 的值不会改变,而是返回一个新的 Tensor。
# 修改自身的数据,比如 x.add_(y) 会直接修改 x 的值,注意这种操作会在方法名后加一个下划线 _ 以示区别。
z = x.add(y) # 不修改 x 的值,返回一个新的 Tensor
print(f'不修改 x 的值:\n{x}\n返回的新 Tensor z:\n{z}')
x.add_(y) # 修改 x 的值
print(f'修改 x 的值:\n{x}')

# <----- 2.2.3 修改 Tensor 形状 ------>
print('*' * 10 + ' 修改 Tensor 形状 ' + '*' * 10)
# 1) size() 返回张量的 shape 属性
x = torch.randn(2, 3, 2)
print(f'原始 Tensor x:\n{x}, 形状: {x.size()}')

# 2) numel(input) 返回张量的元素个数
print(f'张量 x 的元素个数: {x.numel()}')
# 3) view(*shape) 修改张量的形状,view 返回的对象与原始张量共享内存,修改其中一个会影响另一个。
# view(*shape) 要求原 Tensor 的内存是连续的,如果不是连续的,需要先调用 contiguous() 方法。
# view(-1) 表示将张量展平成一维,-1 表示这一维的大小由其他维度推断出来。
y = x.view(3, 4) # 修改形状为 3x4
print(f'修改形状后的 Tensor y:\n{y}, 形状: {y.size()}')
z = x.view(-1) # 展平成一维
print(f'展平成一维的 Tensor z:\n{z}, 形状: {z.size()}')
# reshape(*shape) 修改张量的形状,如果底层内存是连续的,它跟 view() 一样,只返回一个新视图(共享内存)。
# 如果底层不是连续的(比如 x.t()),它会自动调用 .contiguous() 再返回新张量。
x = torch.arange(12).view(3, 4)
y = x.t() # 转置,导致非连续
print(f'y.t() 后张量连续吗? {y.is_contiguous()}') # False
# y.view(12) # 会报错
# RuntimeError: view size is not compatible...
z = y.reshape(-1) # OK,自动复制出连续张量
print(f'使用 reshape 修改内存不连续张量的形状: {z}')

# 4) squeeze(input, dim=None) 去掉张量中维度为 1 的维度,如果指定 dim,则只去掉该维度(如果该维度不为 1,则不变)。
x = torch.randn(1, 3, 1, 4)
print(f'原始 Tensor x:\n{x}, 形状: {x.size()}')
y = x.squeeze() # 去掉所有维度为 1 的维度
print(f'squeeze 后的 Tensor y:\n{y}, 形状: {y.size()}')
z = x.squeeze(0) # 去掉第 0 维(如果为 1)
print(f'squeeze(0) 后的 Tensor z:\n{z}, 形状: {z.size()}')
w = x.squeeze(2) # 去掉第 2 维(如果为 1)
print(f'squeeze(2) 后的 Tensor w:\n{w}, 形状: {w.size()}')

# 5) unsqueeze(input, dim) 在指定位置插入维度为 1 的维度
x = torch.randn(3, 4)
print(f'原始 Tensor x:\n{x}, 形状: {x.size()}')
y = x.unsqueeze(0) # 在第 0 维插入维度为 1 的维度
print(f'unsqueeze(0) 后的 Tensor y:\n{y}, 形状: {y.size()}')
z = x.unsqueeze(2) # 在第 2 维插入维度为 1 的维度
print(f'unsqueeze(2) 后的 Tensor z:\n{z}, 形状: {z.size()}')

# 6) transpose(input, dim0, dim1) 交换张量的两个维度
x = torch.randn(2, 3, 4)
print(f'原始 Tensor x:\n{x}, 形状: {x.size()}')
y = x.transpose(0, 1) # 交换第 0 维和第 1 维
print(f'transpose(0, 1) 后的 Tensor y:\n{y}, 形状: {y.size()}')

# <----- 2.2.4 Tensor 的索引操作 ------>
torch.manual_seed(100) # 设置随机种子
x = torch.randn(2, 3) # 生成 2x3 的数据
print(f'原始 Tensor x:\n{x}')
print(f'获取第一行所有数据: {x[0, :]}')
print(f'获取最后一列的数据: {x[:, -1]}')
# 生成是否大于 0 的 Byter 张量
mask = x > 0 # 创建布尔掩码,标记大于 0 的元素
print(f'获取 x > 0 的 mask :\n{mask}, 类型: {mask.dtype}')
# torch.masked_select(input, mask) 使用二元值进行选择
print(f'利用 masked_select 获取大于 0 的值: {torch.masked_select(x, mask)}')
# torch.nonzero(input) 获取非 0 元素的下标
print(f'利用 nonzero 获取非 0 下标: {torch.nonzero(x)}')
index = torch.LongTensor([[0, 1, 1]]) # 指定索引
# torch.gather(input, dim, index) 根据索引在指定维度收集元素
# 获取指定索引对应的值,输出的形状与 index(必须是 LongTensor) 相同
# 输出规则:
# out[i][j] = input[index[i][j]][j] # dim=0
# out[i][j] = input[i][index[i][j]] # dim=1
result = torch.gather(x, 0, index=index) # 在第 0 维根据 index 收集元素
# dim=0 意味着我们在行维度上选择,对于输出的每个位置 result[i, j],我们从 x[index[i, j], j] 取值
# result[0][0] = x[index[0][0]][0] = x[0][0]
# result[0][1] = x[index[0][1]][1] = x[1][1]
# result[0][2] = x[index[0][2]][2] = x[1][2]
print(f'利用 gather 在第 0 维根据 index 收集元素: {torch.gather(x, 0, index=index)}')
index = torch.LongTensor([[0, 1, 1], [1, 1, 1]])
a = torch.gather(x, 1, index=index) # 在第 1 维根据 index 收集元素
# dim=1 意味着我们在列维度上选择,对于输出的每个位置 a[i, j],我们从 x[i, index[i, j]] 取值
# a[0][0] = x[0][index[0][0]] = x[0][0]
# a[0][1] = x[0][index[0][1]] = x[0][1]
# a[0][2] = x[0][index[0][2]] = x[0][1]
# a[1][0] = x[1][index[1][0]] = x[1][1]
# a[1][1] = x[1][index[1][1]] = x[1][1]
# a[1][2] = x[1][index[1][2]] = x[1][1]
print(f'利用 gather 在第 1 维根据 index 收集元素: {a}')
# torch.scatter_(input, dim, index, src) gather 的反操作,根据索引在指定维度补充数据
z = torch.zeros(2, 3) # 创建一个 2x3 的全零张量
z.scatter_(1, index, a)
print(f'利用 scatter_ 在第 1 维根据 index 补充数据: {z}')

# <----- 2.2.5 PyTorch 的广播操作 ------>
A = torch.arange(0, 40, 10).reshape(4, 1) # shape: 4x1
B = torch.arange(0, 3) # shape: 3
C = A + B # 广播机制,shape: 4x3
print(f'C = A + B 的结果:\n{C}, 形状: {C.size()}')
# 手动实现广播机制
B1 = B.unsqueeze(0) # 在第 0 维插入维度为 1 的维度,shape: 1x3
A1 = A.expand(4, 3) # 在第 1 维扩展,shape: 4x3
B2 = B1.expand(4, 3) # 在第 0 维扩展,shape: 4x3
C1 = A1 + B2
print(f'手动实现广播机制的结果:\n{C1}, 形状: {C1.size()}')

# <----- 2.2.6 逐元素操作 ------>
# 常见的函数有 add(input, other)、sub(input, other)、mul(或*)(input, other)、div(input, other)
# pow(input, exponent)、sqrt(input)、abs(input)、exp(input)、log(input)、clamp(input, min, max)
# ceil(input)、floor(input)、round(input)、sigmoid(input)、tanh(input)、sin(input)、cos(input)
# addcdiv(input, value, tensor1, tensor2) 计算 input + value * (tensor1 / tensor2)
# addcmul(input, value, tensor1, tensor2) 计算 input + value * (tensor1 * tensor2)
# 以上这些操作都支持广播机制
# 这些操作会返回一个新的张量,原始张量不变,如果想要修改原始张量,可以使用对应的原地操作,比如 add_()、sub_() 等
t = torch.randn(1, 3) # 生成 1x3 的数据
t1 = torch.randn(3, 1) # 生成 3x1 的数据
t2 = torch.randn(1, 3) # 生成 1x3 的数据
# t + 0.1*(t1 / t2)
ans = torch.addcdiv(t, value=0.1, tensor1=t1, tensor2=t2) # 等价于 t + 0.1*(t1 / t2)
print(f'利用 addcdiv 计算 t + 0.1*(t1 / t2):\n{ans}, 形状: {ans.size()}')
print(f'计算 sigmoid(t):\n{torch.sigmoid(t)}, 形状: {torch.sigmoid(t).size()}')
print(f'将 t 限制在 [0, 1] 之间:\n{torch.clamp(t, min=0, max=1)}, 形状: {torch.clamp(t, min=0, max=1).size()}')
print(f'进行 t 原地加 1 操作:\n{t.add_(1)}, 形状: {t.size()}')

# <----- 2.2.7 归并操作 ------>
# 归并就是将多个元素归并成一个元素,输入和输出的形状一般不相同,往往是输入大于输出。
# 归并可以对整个张量进行,也可以指定维度进行。
# 常见的归并操作有:
# sum(input, dim, keepdim=False) 对指定维度进行求和,dim 指定维度,keepdim 指定是否保留该维度(含 1 的维度)(默认不保留)
# cumsum(input, dim) 对指定维度进行累加、cumprod(input, dim) 对指定维度进行累乘
# mean(input, dim, keepdim=False) 对指定维度进行求均值
# std(input, dim, keepdim=False, unbiased=True) 对指定维度进行求
# var(input, dim, keepdim=False, unbiased=True) 对指定维度进行求方差
# min(input, dim, keepdim=False) 对指定维度进行求最小值
# max(input, dim, keepdim=False) 对指定维度进行求最大值
# argmin(input, dim) 对指定维度进行求最小值的索引
# argmax(input, dim) 对指定维度进行求最大值的索引
# norm(input, p='fro', dim=None, keepdim=False) 计算张量的范数,p 指定范数类型,dim 指定维度
a = torch.linspace(0, 10, 6) # 生成一个一维张量
a = a.view(2, 3) # 修改形状为 2x3
print(f'原始张量 a:\n{a}, 形状: {a.size()}')
print(f'沿着 y 轴方向累加:{torch.sum(a, dim=0)}, 形状: {a.sum(dim=0).shape}') # 形状为 [3]
print(f'沿着 y 轴方向累加,保留维度:{torch.sum(a, dim=0, keepdim=True)}, 形状: {a.sum(dim=0, keepdim=True).shape}') # 形状为 [1, 3]

# <----- 2.2.8 比较操作 ------>
# 比较操作一般进行逐元素比较,有些操作可以指定维度进行比较。
# 常见的比较操作有:
# eq(input, other) 逐元素比较是否相等,返回布尔张量
# ne(input, other) 逐元素比较是否不等,返回布尔张量
# ge / le / gt / lt(input, other) 逐元素比较是否大于/小于/大于等于/小于等于,返回布尔张量
# max / min(input, axis) 返回最大值/最小值,若指定 axis 则额外返回索引
# topk(input, k, dim=None, largest=True, sorted=True) 返回前 k 个最大/最小值及其索引
x = torch.linspace(0, 10, 6).view(2, 3)
print(f'原始张量 x:\n{x}')
print(f'求所有元素的最大值:\n{torch.max(x)}')
print(f'求沿着 y 方向的最大值:\n{torch.max(x, 0)}')
print(f'求 y 方向最大的 2 个值:\n{torch.topk(x, 2, dim=0)}')

# <----- 2.2.9 矩阵操作 ------>
# 矩阵操作一般有 2 种,一种是逐元素操作,一种是矩阵(点积)乘法。
# 常见的矩阵操作有:
# dot(input, other) 计算两个张量(1维)的点积
# mm(input, mat2) 矩阵乘法,input 和 mat2 都是二维张量
# bmm(input, mat2) 批量矩阵乘法,input 和 mat2 都是三维张量
# matmul(或@)(input, other) 矩阵乘法,input 和 other 可以是任意维度的张量
# t(input) 转置二维张量
# svd(input, some=True, compute_uv=True) 奇异值分解
# 注意:NumPy 中的 dot() 函数可以计算任意维度张量的点积,而 PyTorch 中的 dot() 只能计算一维张量的点积。
# 对于多维张量的点积,可以使用 matmul() 或 @ 运算
# 转置运算会导致内存不连续,如果需要使用 view() 修改形状,需要先调用 contiguous() 方法
a = torch.tensor([2, 3])
b = torch.tensor([3, 4])
print(f'一维张量 a 和 b 的点积: {torch.dot(a, b)}, 形状: {a.dot(b).shape}') # 2*3 + 3*4 = 6 + 12 = 18
a = torch.randint(10, (2, 3))
b = torch.randint(6, (3, 4))
print(f'二维张量的矩阵乘积:\n{torch.mm(a, b)}, 形状: {a.mm(b).shape}') # 形状: 2x4
a = torch.randint(10, (2, 3, 4))
b = torch.randint(6, (2, 4, 5))
print(f'三维张量的批量矩阵乘积:\n{torch.bmm(a, b)}, 形状: {a.bmm(b).shape}') # 形状: 2x3x5
print(f'使用 matmul 计算:\n{a @ b}, 形状: {a.matmul(b).shape}') # 形状: 2x3x5

# <----- 2.2.10 PyTorch 与 NumPy 比较 ------>
# PyTorch 和 NumPy 有很多相同的函数名,但有些函数名不同,功能相同。所以对主要函数进行对比:
# ----------------+------------------------------+-------------------------------------------
# | 操作类别 | NumPy | PyTorch
# ----------------+------------------------------+-------------------------------------------
# | 数据类型 | np.ndarray | torch.Tensor
# | | np.float32 | torch.float32; torch.float
# | | np.float64 | torch.float64; torch.double
# | | np.int32 | torch.int32; torch.int
# | | np.int64 | torch.int64; torch.long
# ----------------+------------------------------+-------------------------------------------
# | 从已有数据构建 | np.array([3.2, 4.3]) | torch.tensor([3.2, 4.3])
# | | x.copy() | x.clone()
# | | np.concatenate | torch.cat
# ----------------+------------------------------+-------------------------------------------
# | 线性代数 | np.dot | torch.mm
# ----------------+------------------------------+-------------------------------------------
# | 属性 | x.ndim | x.dim()
# ----------------+------------------------------+-------------------------------------------
# | 形状 | x.reshape | x.resize; x.view
# | | x.flatten | x.view(-1)
# ----------------+------------------------------+-------------------------------------------
# | 比较 | np.less | torch.lt
# | | np.less_equal / np.greater | torch.le / torch.gt
# | | np.greater_equal / np.equal / np.not_equal | torch.ge / torch.eq / torch.ne
# ----------------+------------------------------+-------------------------------------------
# | 随机种子 | np.random.seed | torch.manual_seed
# ----------------+------------------------------+-------------------------------------------

2.3 Tensor 与 autograd

import torch

# <====== 2.3 Tensor 与 autograd =====>
# <----- 2.3.1 自动求导要点 ------>
# autograd 是 PyTorch 中自动求导的包,autograd 包的两个核心类是 torch.Tensor 和 torch.Function。
# | 属性 | 含义 | 何时存在 |
# | --------------- | ---------------------| ----------------------------------------|
# | `requires_grad` | 是否记录梯度 | 由用户或操作继承 |
# | `grad_fn` | 用于反向传播的函数节点 | 由计算生成的张量自动带上 |
# | `grad` | 存放反向传播后得到的梯度值 | `.backward()` 执行后出现(通常在叶子张量上) |
# 在求导时需要考虑以下几点:
# 1) 创建叶子结点 (leaf node) 的 Tensor,使用 requires_grad 参数指定是否记录对其的操作,以便之后利用 backward 函数进行梯度计算。
# requires_grad 默认为 False,如果需要对该 Tensor 求导,则必须将其设置为 True,与之有依赖关系的结点也会被自动设置为 True。
# 2) 可以利用 requires_grad() 方法修改 Tensor 的 requires_grad 属性。可以调用 .detach() 或 with torch.no_grad(): 不再记录张量的梯度、跟踪张量的历史记录。
# 这点在评估模型、测试模型阶段经常使用。
# 3) 通过运算创建的 Tensor(即非叶子结点),会自动被赋予 grad_fn 属性。该属性表示梯度函数。叶子结点的 grad_fn 属性为 None。
# 4) 最后得到的 Tensor 执行 backward() 函数时,会自动计算各变量的梯度,并将累加结果保存到 grad 属性中。计算完成后,非叶子结点的梯度会被释放。
# 5) backward 函数接受参数,该参数应该与调用 backward 函数的 Tensor 的维度相同,或者是可以广播的维度。
# 如果求导的 Tensor 是标量(即只有一个元素),则不需要为 backward 函数提供任何参数。
# 6) 反向传播的中间缓存会被释放,以节省内存。如果需要多次调用 backward 函数,则需要在第一次调用时传入 retain_graph=True 参数。多次反向传播会累加梯度。
# 7) 非叶子结点的梯度在每次反向传播后会被释放。
# 8) 可以通过 torch.no_grad() 包裹代码块来阻止 autograd 去跟踪那些标记为 .requires_grad=True 的 Tensor 的历史记录。
# 在整个过程种,PyTorch 采用动态图机制,动态构建计算图(有向无环图,DAG)。它的计算图在每次正向传播时,将重新构建。

# <----- 2.3.2 标量的反向传播 ------>
# 标量的反向传播是指对单个数值进行求导。标量的反向传播相对简单,因为它只有一个输出值。
# PyTorch 使用 torch.autograd.backward() 函数来执行反向传播,函数的的具体格式如下:
# torch.autograd.backward(
# tensors,
# gradient=None,
# retain_graph=None,
# create_graph=False)
# tensors: 需要进行反向传播的张量,可以是单个张量或者张量的元组。
# gradient: 当 y 不是标量时,需要手动告诉 PyTorch “每个输出元素在反向传播中要乘以多少权重”。形状与输入张量相同。
# retain_graph: 通常在调用一次 backward 函数后,计算图会被释放。如果相对一个变量重复调用 backward 函数,则需要将该参数设置为 True。
# create_graph: 如果需要计算高阶导数,则需要将该参数设置为 True。

# 假设对 z = wx + b 求导,x, w, b 都是标量。那么对标量 z 求导调用 backward 函数时,不需要传入任何参数。
# 下面实现自动求导:
# step1: 定义叶子结点和算子结点
x = torch.Tensor([2]) # 定义输入 x (标量)
# 初始化权重 w 和偏置 b,设置 requires_grad=True 以便后续求导
w = torch.randn(1, requires_grad=True) # 定义权重 w (标量)
b = torch.randn(1, requires_grad=True) # 定义偏置 b (标量)
# 前向传播
y = w * x # 等价于 torch.mul(w, x)
z = y + b # 等价于 torch.add(y, b)
# 查看 x, w, b 叶子结点的 requires_grad 和 grad_fn 属性
print(f'x, w, b 的 requires_grad 分别为: {x.requires_grad}, {w.requires_grad}, {b.requires_grad}')
# step2 : 查看叶子结点、非叶子结点的其他属性
# 查看非叶子结点 y, z 的 requires_grad 属性
print(f'y, z 的 requires_grad 分别为: {y.requires_grad}, {z.requires_grad}')
# 因为与 w, b 有依赖关系,所以 y, z 的 requires_grad 属性均为 True
# 查看各结点是否为叶子结点
print(f'x, w, b, y, z 是否为叶子结点: {x.is_leaf}, {w.is_leaf}, {b.is_leaf}, {y.is_leaf}, {z.is_leaf}')
# 查看叶子结点的 grad_fn 属性
print(f'x, w, b 的 grad_fn 分别为: {x.grad_fn}, {w.grad_fn}, {b.grad_fn}')
# 因为 x, w, b 是用户创建的,通过运算创建的结点才有 grad_fn 属性,所以 x, w, b 的 grad_fn 属性均为 None
# 查看非叶子结点的 grad_fn 属性
print(f'y, z 的 grad_fn 分别为: {y.grad_fn}, {z.grad_fn}')
# step3: 调用 backward 函数,自动求导
# 对标量 z 求导时,不需要传入任何参数
z.backward()
# 如果需要多次调用 backward 函数,则需要在第一次调用时传入 retain_graph=True 参数
# z.backward(retain_graph=True)
# 查看叶子结点的梯度,x 是叶子结点,但不需要对 x 求导,所以 x 的梯度为 None
print(f'x, w, b 的梯度分别为: {x.grad}, {w.grad}, {b.grad}')
# w 的梯度为 [2.],b 的梯度为 [1.]
# 查看非叶子结点的梯度,非叶子结点的梯度在每次反向传播后会被释放
print(f'y, z 的梯度分别为: {y.grad}, {z.grad}')
# y, z 的梯度均为 None

# <----- 2.3.3 非标量的反向传播 ------>
# PyTorch 有一个原则,不让张量对张量求导,只允许标量对张量求导。也就是说,只有标量才能调用 backward 函数。
# 所以如果目标张量对一个非标量调用 backward 函数,需要传入一个 gradient_tensors 参数。
# 该参数的形状需要与调用 backward 函数张量的形状保持一致。
# 传入 gradient 参数的含义是为了把张量对张量的求导转换为标量对张量求导。
# 假设目标值 loss = [y1, y2, ..., ym],每个 yi 都是标量,那么 loss 对于某个变量 x 的导数是一个向量 [d(y1)/dx, d(y2)/dx, ..., d(ym)/dx]。
# 该向量的每个元素都可以通过标量对 x 求导得到
# 但是 PyTorch 不允许直接对非标量调用 backward 函数,所以需要传入一个与 loss 形状相同的张量 v = [v1, v2, ..., vm]。
# 该张量的每个元素 vi 可以看作是 loss 中每个标量 yi 对 x 的导数的权重。
# 这样,loss 对 x 的导数就可以通过标量对 x 求导得到,即 d(v1*y1 + v2*y2 + ... + vm*ym)/dx。
# 先看一个简单的例子:
X = torch.ones(2, requires_grad=True) # 定义输入 X (2 行 1 列)
Y = X ** 2 + 3
# Y.backward() # 报错,Y 不是标量,不能直接调用 backward 函数
# X = [x1, x2] Y = [y1, y2] = [x1^2 + 3, x2^2 + 3],可以把 Y 变成标量 y = y1 + y2,然后对 y 求导也是可以的
Y.sum().backward()
print(f'X 的梯度为: {X.grad}') # X 的梯度为: tensor([2., 2.])
X = torch.ones(2, requires_grad=True) # 重新定义输入 X (2 行 1 列)
Y = X ** 2 + 3
Y.backward(gradient=torch.tensor([1., 1.]))
print(f'直接使用 gradient 参数计算 X 的梯度也是一样的: {X.grad}') # 直接使用gradient参数计算 X 的梯度也是一样的: tensor([2., 2.])
# 下面看一个复杂的例子:
# 定义叶子结点张量 x,形状为 2x2
x = torch.tensor([[2, 3]], dtype=torch.float, requires_grad=True)
# 初始化雅可比矩阵
J = torch.zeros(2, 2)
# 初始化目标张量,形状为 1x2
y = torch.zeros(1, 2)
# 定义 y 和 x 之间的映射关系
# y1 = x1^2 +3x2, y2 = x2^2 + 2x1
y[0, 0] = x[0, 0] ** 2 + 3 * x[0, 1]
y[0, 1] = x[0, 1] ** 2 + 2 * x[0, 0]
# 先手动计算一下 y 对 x 的雅可比矩阵
# J = [[dy1/dx1, dy1/dx2], [dy2/dx1, dy2/dx2]] = [[2x1, 3], [2, 2x2]]
J[0, 0] = 2 * x[0, 0]
J[0, 1] = 3
J[1, 0] = 2
J[1, 1] = 2 * x[0, 1]
print(f'手动计算的雅可比矩阵 J 为:\n{J}')
# 下面使用 autograd 自动计算雅可比矩阵
# 对 y1 求导
y.backward(torch.Tensor([[1, 0]]), retain_graph=True) # retain_graph=True 保留计算图,以便后续对 y2 求导
J[0] = x.grad # 将 y1 对 x 的导数存入雅可比矩阵的第一行
# 梯度是累加的,所以在对 y2 求导前,需要将 x 的梯度清零
x.grad.zero_() # 清零梯度
# 对 y2 求导
y.backward(torch.Tensor([[0, 1]]))
J[1] = x.grad # 将 y2 对 x 的导数存入雅可比矩阵的第二行
print(f'调用 backward 获取张量对张量的梯度:\n{J}')
# 小结一下:
# 1) PyTorch 不允许张量对张量求导,只允许标量对张量求导。
# 2) 为了避免张量对张量求导,可以利用 torch.autograd.backward() 函数的 gradient 参数,将张量对张量求导转换为标量对张量求导。
# y.backward(gradient=v) 的含义是对: 先计算 loss = torch.sum(y*v),也就是: v1*y1 + v2*y2 + ... + vm*ym,然后求 loss 对所以变量 x 的导数。y和v形状相同。
# 也就是说,可以理解成先按照 v 对 y 进行加权求和,得到一个标量 loss,然后对 loss 求导。
# 3) PyTorch 采用动态图机制,动态构建计算图(有向无环图,DAG)。它的计算图在每次正向传播时,将重新构建。反向传播后计算图立即销毁。

# <----- 2.3.4 切断一些分支的反向传播 ------>
# 在训练网络时,有时候我们希望保持一部分网络的参数不变,只对其中一部分网络进行训练(迁移学习)。这时候可以使用 detach() 方法来切断一些分支的反向传播。
# detach() 将张量从创建它的计算图中分离出来,把它当作叶子结点,参数 requires_grad=False 且 grad_fn=None。
x = torch.ones(2, requires_grad=True) # [1. , 1.]
y = x ** 2 + 3
# 分离变量 y,不再计算 y 对 x 的梯度
c = y.detach() # c = [4., 4.]
z = c * x
z.sum().backward()
print(f'x 的梯度为: {x.grad}, {x.grad == c}') # 因为 c 不再计算梯度,所以当作常数处理, dz/dx = c
print(f'c 能否进行反向传播: {c.grad_fn}, {c.requires_grad}')
x.grad.zero_() # 清零梯度
y.sum().backward()
print(f'x 的梯度为: {x.grad}, {x.grad == 2 * x}') # 因为 y 计算梯度,所以当作变量处理, dy/dx = 2x

# <----- 2.3.5 tips ------>
x = torch.tensor([1., 2., 3.], requires_grad=True)
y = x ** 2
loss = y.sum()
loss.backward(retain_graph=True)
print(f'使用loss.sum() 计算梯度: {x.grad}')

x.grad.zero_() # 清零梯度
loss = y.mean()
loss.backward()
print(f'使用loss.mean() 计算梯度: {x.grad}') # 和前面的梯度值差了 3 倍,刚好是一个 batch 的大小
# 因为 loss = Σy_i, d(loss) / d(y_i) = 1
# 而 loss = 1 / N Σ(y_i), d(loss) / d(y_i) = 1 / N
# 所以使用 mean 计算的梯度比使用 sum 计算的梯度少了一个 batch 的大小 N
# sum() 不改变梯度的数值,只是把所有 y_i 的梯度都传回去
# mean() 会把梯度除以 batch 的大小 N

2.4 分别使用 NumPy 和 PyTorch 实现线性回归

import numpy as np
import torch
from matplotlib import pyplot as plt

# <====== 2.4 分别使用 NumPy 和 PyTorch 实现线性回归 =====>
# <----- NumPy ----->
# 生成输入数据 x 和目标数据 y
np.random.seed(100)
x = np.linspace(-1, 1, 100).reshape(100, 1) # 100 行 1 列
y = 3 * np.pow(x, 2) + 2 + 0.2 * np.random.randn(x.size).reshape(100, 1) # y = 3x^2 + 2 + 噪声

# 查看 x 和 y 的分布
plt.scatter(x, y)
plt.show()

# 随机初始化参数
w1 = np.random.rand(1, 1) # 1 行 1 列
b1 = np.random.rand(1, 1)

# 定义训练模型
lr = 1e-3 # 学习率
for epoch in range(800):
# 前向传播
y_pred = np.power(x, 2) * w1 + b1 # 广播机制: 100 行 1 列
# 定义损失函数: loss = 0.5 * (y_pred - y)^2
loss = 0.5 * np.pow((y_pred - y), 2) # 100 行 1 列
loss = loss.sum() / x.shape[0] # 损失函数取均值
# 反向传播,计算梯度
grad_w = np.sum((y_pred - y) * np.power(x, 2)) # d(loss)/dw = (y_pred - y) * x^2
grad_b = np.sum((y_pred - y)) # d(loss)/db = (y_pred - y)
# 更新参数
w1 -= lr * grad_w
b1 -= lr * grad_b

# 训练结束,打印结果
y_pred = np.power(x, 2) * w1 + b1
loss = 0.5 * np.pow((y_pred - y), 2).sum()
print(f'Final: loss = {loss}, w1 = {w1}, b1 = {b1}')
# 画图对比
plt.scatter(x, y)
plt.plot(x, y_pred, 'r-', lw=2)
plt.show()


# <----- PyTorch ----->
torch.manual_seed(100)
# 生成输入数据 x 和目标数据 y
x = torch.linspace(-1, 1, 100).view(100, 1) # 100 行 1 列
y = 3 * x.pow(2) + 2 + 0.2 * torch.randn(x.size()) # y = 3x^2 + 2 + 噪声

# 画图查看 x 和 y 的分布(要把 x 和 y 转为 numpy)
plt.scatter(x.numpy(), y.numpy())
plt.show()

# 随机初始化参数
w2 = torch.randn(1, 1, requires_grad=True) # 1 行 1 列
b2 = torch.randn(1, 1, requires_grad=True) # 1 行 1 列
# 为什么 w2 和 b2 必须是 1x1 的矩张量 ? 因为后面用到了矩阵乘法 mm(),必须输入二维矩阵

# 定义训练模型
lr = 1e-3 # 学习率
for epoch in range(800):
# 前向传播
y_pred = x.pow(2).mm(w2) + b2
loss = 0.5 * (y_pred - y).pow(2)
loss = loss.sum()
# 反向传播,计算梯度
loss.backward() # 标量不需要传入参数
# 更新参数
with torch.no_grad(): # 禁止梯度更新
w2 -= lr * w2.grad
b2 -= lr * b2.grad
# 因为 autograd 会累加梯度,所以每次更新后需要把梯度清零
w2.grad.zero_()
b2.grad.zero_()

y_pred = x.pow(2).mm(w2) + b2
loss = 0.5 * (y_pred - y).pow(2).sum()
print(f'Final: loss = {loss.item()}, w2 = {w2.item()}, b2 = {b2.item()}')
plt.plot(x.detach().numpy(), y_pred.detach().numpy(), 'r-', label='predict', linewidth=4)
plt.scatter(x.detach().numpy(), y.detach().numpy(), label='real', color='blue', marker='o')
plt.legend()
plt.show()

2.5 使用优化器和自动微分实现线性回归

import numpy as np
import torch
import torch.nn as nn # 导入神经网络模块,第三章会讲到
from matplotlib import pyplot as plt

# <====== 2.5 使用优化器和自动微分实现线性回归 =====>
# 在 2.4 节中,我们使用 NumPy 和 PyTorch 分别实现了线性回归。其中,PyTorch 版本中,我们手动实现了反向传播,计算了梯度,并更新了参数。
# 其实,PyTorch 已经为我们实现好了这些功能,我们只需要调用相应的接口即可。下面我们将使用 PyTorch 的优化器(optimizer)和自动微分(autograd)功能来实现线性回归。
# 这样,我们就不需要手动计算梯度和更新参数了。

torch.manual_seed(100)
# 生成输入数据 x 和目标数据 y
x = torch.linspace(-1, 1, 100).view(100, 1) # 100 行 1 列
y = 3 * x.pow(2) + 2 + 0.2 * torch.randn(x.size()) # y = 3x^2 + 2 + 噪声

# 画图查看 x 和 y 的分布(要把 x 和 y 转为 numpy)
plt.scatter(x.numpy(), y.numpy())
plt.show()

# 随机初始化参数
w = torch.randn(1, 1, requires_grad=True) # 1 行 1 列
b = torch.randn(1, 1, requires_grad=True) # 1 行 1 列
# 定义损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失函数
optimizer = torch.optim.SGD(params=[w, b], lr=1e-2) # 随机梯度下降优化器,params 指定要更新的参数,lr 指定学习率

# 训练模型
for epoch in range(800):
# 前向传播
y_pred = x.pow(2).mm(w) + b
loss = criterion(y_pred, y) # 计算损失
# 反向传播,计算梯度
loss.backward() # 标量不需要传入参数
# 更新参数
optimizer.step() # 自动更新参数
optimizer.zero_grad() # 清零梯度


y_pred = x.pow(2).mm(w) + b
loss = 0.5 * (y_pred - y).pow(2).sum()
print(f'Final: loss = {loss.item()}, w2 = {w.item()}, b2 = {b.item()}')
plt.plot(x.detach().numpy(), y_pred.detach().numpy(), 'r-', label='predict', linewidth=4)
plt.scatter(x.detach().numpy(), y.detach().numpy(), label='real', color='blue', marker='o')
plt.legend()
plt.show()

2.6 把数据集转为带批量处理功能的迭代器

import numpy as np
import torch
import torch.nn as nn
from matplotlib import pyplot as plt


# <====== 2.6 把数据集转为带批量处理功能的迭代器 =====>
# 在 2.5 节中,我们使用 PyTorch 的优化器和自动微分功能实现了线性回归。
# 但是,我们在每次迭代中都使用了全部的数据进行训练,这种方式称为批量梯度下降(Batch Gradient Descent)。
# 在实际应用中,数据集通常比较大,一次性使用全部数据进行训练会占用大量内存,并且计算效率较低。
# 因此,我们通常会把数据集分成若干个小批量(mini-batch),每次使用一个小批量的数据进行训练,这种方式称为小批量梯度下降(Mini-batch Gradient Descent)。
# PyTorch 提供了一个非常方便的工具——DataLoader,可以帮助我们把数据集转换为带批量处理功能的迭代器。
# 但是这里我们不使用 DataLoader,而是手动实现一个简单的带批量处理功能的迭代器。


def data_iter(features, labels, batch_size=4):
"""
生成一个带批量处理功能的迭代器
:param features: 输入数据矩阵,每一行是一个样本的特征。形状通常是 [num_examples, num_features]。
:param labels: 每个样本对应的标签(监督学习的目标),形状通常是 [num_examples, 1]。
:param batch_size: 每个批量的样本数量,默认为 4。
:return: 一个生成器,每次迭代返回一个批量的样本和标签。
"""
num_examples = len(features) # 样本数量
indices = list(range(num_examples)) # 样本索引
np.random.shuffle(indices) # 样本索引随机排序
for i in range(0, num_examples, batch_size):
index = torch.LongTensor(indices[i: min(i + batch_size, num_examples)]) # 当前批量的样本索引,同时考虑最后一批样本可能不足 batch_size 的情况
# yield 关键字让函数变成一个生成器 (generator),而不是普通函数。
# 普通函数 生成器函数(带yield)
# 用return 返回一次性结果 用yield 每次返回一部分结果
# 执行完就退出 每次执行到 yield 暂停,下次从原位置继续
# 一次性加载所有数据 可节省内存、边取边用
yield features.index_select(0, index), labels.index_select(0, index) # 返回当前批量的样本和标签


def data_iter_test():
x = torch.arange(7).view(7, 1)
y = torch.arange(7).view(7, 1)
for features, labels in data_iter(x, y, 4):
print(features, labels)
print('=====')

# 定义 x, y 以及其他参数
torch.manual_seed(100)
# 生成输入数据 x 和目标数据 y
x = torch.linspace(-1, 1, 100).view(100, 1) # 100 行 1 列
y = 3 * x.pow(2) + 2 + 0.2 * torch.randn(x.size()) # y = 3x^2 + 2 + 噪声

# 随机初始化参数
w = torch.randn(1, 1, requires_grad=True) # 1 行 1 列
b = torch.randn(1, 1, requires_grad=True) # 1 行 1 列
# 定义损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失函数
optimizer = torch.optim.SGD(params=[w, b], lr=1e-2) # 随机梯度下降优化器,params 指定要更新的参数,lr 指定学习率

# 训练模型
for i in range(1000):
for features, labels in data_iter(x, y, 10):
# forward
y_pred = features.pow(2).mm(w) + b
loss = criterion(y_pred, labels) # 计算损失
# backward
loss.backward() # 标量不需要传入参数
# update
optimizer.step() # 自动更新参数
optimizer.zero_grad() # 清零梯度

y_pred = x.pow(2).mm(w) + b
loss = 0.5 * (y_pred - y).pow(2).sum()
print(f'Final: loss = {loss.item()}, w2 = {w.item()}, b2 = {b.item()}')
plt.plot(x.detach().numpy(), y_pred.detach().numpy(), 'r-', label='predict', linewidth=4)
plt.scatter(x.detach().numpy(), y.detach().numpy(), label='real', color='blue', marker='o')
plt.legend()
plt.show()

if __name__ == '__main__':
data_iter_test()