神经网络与深度学习-Pytorch
PyTorch是一个用于机器学习和深度学习的开源深度学习框架,由Facebook于2016年发布,其主要实现了自动微分功能,并引入动态计算图使模型建立更加灵活。Pytorch可分为前后端两个部分,前端是与用户直接交互的python API,后端是框架内部实现的部分,包括Autograd,它是一个自动微分引擎。
Pytorch基于已有的张量库Torch开发,在PyTorch的早期版本中,使用的是Torch7,后来随着PyTorch的发展,逐渐演变成了PyTorch所使用的张量库。
现如今,Pytorch已经成为开源机器学习系统中,在科研领域市场占有率最高的框架,其在AI顶会上的占比在2022年已达80% 。
张量的类型转换
张量转换NumPy数组
1 | def dem02(): |
NumPy数组转换张量
- 使用from_ numpy 可以将ndarray数组转换为Tensor,默认共享内存,使用 copy 函数避免共享。
- 使用 torch.tensor 可以将 ndarray 数组转换为 Tensor,默认不共享内存。(用的多)
1 | def dem02(): |
从张量中提取内容
只能从标量张量中提取内容。也就是这个张量中只能有一个值
1 | def dem02(): |
张量的数值计算
基本运算
加减乘除
涉及到的API:
- add(), sub(), mul(), div(), neg() 加减乘除,取反substact,multiply,divide
- add_(), sub_(), mul_() div_(), neg_()功能同上,只不过可以修改源数据,类似于 Pandas部分的inplace=True
需要你记忆的: +,-,*,/
如果是张量和数值运算,则:该数值会和张量中的每个值依次进行 对应的运算.
1 | def dem02(): |

幂运算:pow()、pow_()
接口:Tensor ** exponent / Tensor.pow(exponent) / Tensor.pow_(exponent)
功能:逐元素求幂。pow() 返回新张量,pow_() 为 in-place。
参数:exponent:指数(标量或与张量可广播的张量)
1 | import torch |
求平方根:sqrt()、sqrt_()
接口:Tensor.sqrt() / Tensor.sqrt_()
功能:逐元素平方根。sqrt() 返回新张量,sqrt_() 为 in-place。
参数:无
1 | import torch |
以 e 为底:exp()、log()
接口:Tensor.exp() / Tensor.exp_()、Tensor.log() / Tensor.log_()
功能:逐元素以 e 为底求幂(exp)或求自然对数(log)。带下划线版本为 in-place。
参数:无
1 | import torch |
哈达玛积(逐元素乘法)
接口:*、Tensor.mul(other)
功能:两张量形状相同或可广播时,对应位置相乘(Hadamard product),与矩阵乘法不同。
参数:other:张量或标量
1 | import torch |
矩阵乘法运算
点乘:
要求:两个张量的维度保持一致,对应元素直接做相应的操作.
API:t1 * t2t1.mul(t2)# multiply: 乘法
矩阵乘法:
要求:两个张量,第一个张量的列数,等于第二个张量 的行数(A列 = B行)
结果:A行B列
API:t1 @ t2t1.matmul(t2)t1.dot(t2)扩展:只针对于一维张量有效.
注意:
X = X @ Y 会先分配新张量再赋给 X;若之后不再使用原来的 X,可用 X[:] = X @ Y 在原有存储上写入,减少一次分配。确保右边的结果能够正确地广播到左边指定的形状。如果形状不匹配,则会导致错误。
1 | import torch |
张量的索引操作
简单索引与切片
接口:tensor[i]、tensor[:, j]、tensor[i:j, k:l] 等下标与切片语法
功能:单整数索引会使该维消失(降维);切片保留该维;: 表示该维全选。
参数:i、j 等为整数或切片(start:end:step),支持负索引。
1 | tensor1 = torch.randint(1, 9, (3, 5, 4)) |
⭐列表索引
接口:tensor[[i1,i2,...],[j1,j2,...],...] 等,下标为整数张量或列表。
:表示选择当前所有行/列
tensor[[[1],[2]],[3,4]]:代表把第2行的3,4两列和第3行的3,4两列读出来tensor[:, tensor[2]>5]: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# tensor[2] > 5 仅以第3个矩阵(索引2)为条件,生成布尔掩码。
# 但 tensor[:, mask] 会在所有矩阵(第1、第2、第3个)的相同位置提取数据,而不管其他矩阵在这 些位置的值是否满足>5
# 结果是所有矩阵在第3个矩阵满足条件的那些位置上的值(共7个位置)。
# 只有第3个矩阵的对应值保证 >5,而第1、第2个矩阵的值只是巧合出现在这些位置,可能完全不符合 >5 的条件。
tensor = torch.tensor([
[[7, 1, 4, 4],
[4, 8, 2, 8],
[7, 8, 7, 2],
[2, 2, 7, 8],
[7, 2, 2, 2]],
[[1, 2, 2, 3],
[7, 5, 7, 3],
[5, 8, 8, 2],
[7, 4, 6, 7],
[5, 5, 4, 1]],
[[4, 2, 2, 7],
[1, 6, 8, 7],
[7, 1, 6, 2],
[4, 4, 3, 1],
[2, 8, 1, 5]]])
print(tensor[:,tensor[2]>5])
# [4, 2, 2, 7],
# [1, 6, 8, 7],
# [7, 1, 6, 2],
# [4, 4, 3, 1],
# [2, 8, 1, 5]
# [[False, False, False, True],
# [False, True, True, True],
# [ True, False, True, False],
# [False, False, False, False],
# [False, True, False, False]]
# 得到布尔掩码后返回所有为True的值t1[:, t1[1, :] > 5](t1[:, t1[1] > 5]):t1[1, :] > 5是一个定位,代表选择所有行的第【在矩阵中第二行列数据大于五的】列1
2
3
4
5
6
7
8
9
10
11
12t1 = torch.tensor([[6, 9, 9, 2, 8],
[7, 8, 5, 8, 4],
[7, 4, 3, 9, 3],
[6, 1, 4, 2, 8],
[1, 2, 5, 7, 4]])
print(t1[:, t1[1, :] > 5])
#输出所有行的第1,2,4列数据。共5行(所有行),3列
# tensor([[6, 9, 2],
# [7, 8, 8],
# [7, 4, 9],
# [6, 1, 2],
# [1, 2, 7]])
功能:多下标按位置配对或广播,可取出不连续或重排后的元素。
参数:各维传入长度相同的整数序列或可广播的整数张量。
1 | import torch |
下面演示一下三维张量的列表索引计算方法
1 | tensor = torch.tensor([ |
⭐范围索引(切片)
同上“简单索引”中的切片用法;start:end:step、负索引均支持。
1 | import torch |
下面演示一下三维张量的范围索引计算方法
1 | tensor = torch.tensor([ |
多维索引
1 | t2 = torch.tensor([ |


索引操作 t2[:,:,0] 的工作原理
当我们使用 t2[:,:,0] 时:
- 第一个 : 表示保留所有层(2个层)
- 第二个 : 表示保留所有行(3行)
- 0 表示只选择第0列
所以这个操作实际上是:
- 从第0层:取第0列 → [3, 8, 4]
- 从第1层:取第0列 → [2, 6, 2]
张量的形状操作
reshape 与 view(View不常用)
接口:
Tensor.reshape(*shape):在内存不连续时可返回副本Tensor.view(*shape):要求张量在内存中连续,否则需先调用contiguous()
功能:在元素总数不变的前提下调整形状。
1 | tensor1 = torch.randint(1, 9, (3, 5, 4)) # 共 60 个元素 |
unsqueeze:增加大小为 1 的维
接口:
Tensor.unsqueeze(dim)Tensor.unsqueeze_(dim)
功能:在指定位置插入一个大小为 1 的维度,常用于广播或与某些接口的维度要求对齐。一般在模型欠拟合的时候增加维度
参数:
dim:插入的维度下标(支持负索引,-1 表示最后一维之后)。
1 | # 1. 定义2行3列的张量. |
squeeze:删除大小为 1 的维
接口:
Tensor.squeeze(dim=None)Tensor.squeeze_(dim=None)
功能:删除大小为 1 的维度。不传 dim 时删除所有大小为 1 的维;传 dim 时仅当该维为 1 才删除。
参数:
dim:可选,指定要删除的维度下标。
1 | import torch |
交换维度
接口:
Tensor.transpose(dim0, dim1):交换两个指定维度Tensor.permute(*dims):按给定顺序重排所有维度,可一次实现多维交换
参数:
transpose(dim0, dim1):dim0、dim1为要交换的两个维度下标(从 0 起)。permute(*dims):*dims为新的维度顺序,例如原形状 (2,3,6) 传入 (2,0,1) 得到 (6,2,3)。
1 | tensor1 = torch.randint(1, 9, (2, 3, 6)) |
张量的拼接操作
torch.cat
接口:torch.cat(tensors, dim=0, out=None)
功能:在指定维度上拼接多个张量,除 dim 外其余维度须相同。
参数:
tensors:张量序列(list 或 tuple)dim:沿该维拼接out:可选输出张量
1 | # 1. 创建两个张量. |

torch.stack
接口:torch.stack(tensors, dim=0, out=None)
功能:在新维度上堆叠多个张量,所有输入形状必须一致,结果多出一维。
参数:
tensors:形状相同的张量序列dim:插入的新维度位置out:可选输出张量
1 | # 1. 创建两个张量. |
关于t8:


关于t9:

⭐这里的二维度相比于前面的维度,只是单个数字,故不用再加中括号


自动微分模块
训练时 PyTorch 会构建计算图(computational graph),记录数据与运算,并通过内置的微分引擎 torch.autograd 在根节点调用 backward() 自动计算梯度。PyTorch 的 backward() 就是在计算图上自动做链式法则,从根节点(通常是损失)一路把梯度传回叶子节点(如权重、偏置)。


1 | # 1. 定义变量,记录:初始的权重w(旧) |
在真实的神经网络中,情况完全不同:
- 损失函数不是 2w²,而是 loss = f(w, x, y) —— 依赖输入数据 x 和真实标签 y。
- 梯度不会总是把 w 推向 0,因为 loss 的最低点不一定在 w=0 处。比如线性回归 loss = (wx - y)²,最优 w 是让 wx ≈ y
的那个值,通常不是 0。
- 多个权重相互制衡,真实网络有成千上万的参数,一个权重的梯度受其他所有权重影响,不会简单归零。
- 当模型收敛时,梯度会趋近于 0,此时权重稳定在最优值附近,而不是单纯的 0。
一句话总结:这个 demo 的 loss 函数恰好是 2w²(最小值在 w=0),所以你看到 w→0
是对的。但在真实场景中,梯度下降会让权重停在让 loss 最小的那个值,那个值一般不是 0。
自动微分模块案例-循环更新参数
1 | w = torch.tensor(data=10, requires_grad=True, dtype=torch.float32) |
案例-线性回归案例
整体流程:准备数据 → 构建模型 → 定义损失与优化器 → 循环训练(前向、算损失、反向、更新参数)。
⭐⭐⭐用到的函数和知识点
PyTorch 里处理数据的标准三步流程
tensor → Dataset → DataLoader
Tensor:原始数据
1
2X = torch.tensor([[1.0, 2.0], [3.0, 4.0], ...])
y = torch.tensor([0.0, 1.0, ...])Tensor 只是装着数字的多维数组本身,它不知道怎么批量取数据、怎么打乱、怎么并行加载。
Dataset:把数据包装成”可索引”的对象
1
2
3from torch.utils.data import TensorDataset
dataset = TensorDataset(X, y)Dataset的作用是:- 把
X和y配对绑定 - 让你可以通过索引拿到一个
(features, label)样本:
x_i, y_i = dataset[0]它回答的是”第 i 个样本是什么”。
- 把
DataLoader:数据加载器
1
2
3from torch.utils.data import DataLoader
loader = DataLoader(dataset, batch_size=32, shuffle=True)数据加载器就是帮你自动把数据集切分成小批量(batch),然后按批次喂给模型的工具。
它主要做这几件事:
功能 作用 batch_size每次取多少条样本 shuffle每个 epoch 是否打乱数据顺序 自动迭代 用 for batch in loader就能逐个 batch 遍历多进程加载 num_workers可以并行读取数据例如:
1
2
3
4
5for X_batch, y_batch in loader:
# X_batch 形状: (32, n_features)
# y_batch 形状: (32,)
pred = model(X_batch)
loss = criterion(pred, y_batch)
创造数据make_regression
make_regression 是 scikit-learn 中一个用来生成模拟回归数据的函数,常用于测试线性回归模型。返回的是 NumPy 数组(ndarray)
它的基本作用:根据你指定的参数,自动构造一个带有已知真实系数的人造数据集,这样你可以验证模型能不能学得够准。
1 | make_regression:我故意设了真实系数,再生成带噪声的数据点 |
常用参数大概长这样:
1 | from sklearn.datasets import make_regression |
它会返回:
X:形状为(n_samples, n_features)的特征矩阵y:形状为(n_samples,)的目标值coef:真实特征的权重系数(如果coef=True)
make_regression 的约定:
x(特征):永远是 2维(100, 1)— 因为特征可以有多个维度y(目标值):永远是 1维(100,)—sklearn的习惯,因为目标值就是一个标量
生成数据的大致公式是:
1 | y = X @ coef + bias + noise # 假设 X 是 (100, 3),coef 是 (3,),结果 y 是形状 (100,) |
其中 noise 是服从正态分布的随机扰动。
在模拟线性回归时,它的好处是你知道“正确答案”——可以把模型学出来的系数和 make_regression 返回的真实 coef 对比,看偏差有多大,用来评估模型效果或者做教学演示。
线性回归函数
理解线性回归函数Linear()的定义
代码
nn.Linear(1, 1)中的 “1” 指的是什么?第一个 1:每个样本的特征数
(in_features)第二个 1:每个样本的输出出数
(out_features)
线性回归公式为
output = input × W^T + bW的形状:(1, 1)— 因为输入1维、输出1维b的形状:(1,)
根据上述公式,如果我们传入
(100, 1)并且代入公式中:input (100, 1) × W^T (1, 1) → (100, 1) + b。实际意义是 “对 batch 里的每个样本,分别做一次
y = x * w + b"。无论传 (1, 1)、(16, 1) 还是 (100, 1),PyTorch 自动把 batch 维(第一维)当作”批次”,对每个样本独立做线性变换。
线性回归运算的本质是只(100,1)的第二维做计算,第一维是做计算的次数。如果传入(100,2)的话,那么线性回归就要为 nn.Linear(2, 1),表示我的线性回归要对两个特征做一百次计算,计算出100个1个特征的样本

另外:⭐nn.Linear()的两个维度通常是不相等的
1 | 相等的情况反而少见。nn.Linear 的作用就是在不同维度之间做映射(投影): |
损失函数
回归问题的损失函数
nn.MSELoss()以均方误差为指导思想来不断逼近真实值
loss = criterion(y_pred,train_y.reshape(-1,1))计算预测值和真实值之间的损失,分两部分解释:
criterion(y_pred, ...) # train_x shape: (16, 1)criterion是一个损失函数(比如nn.MSELoss()或nn.BCELoss()),计算预测值y_pred
和真实值之间的差异。返回一个标量(张量),数值越小说明预测越准。train_y.reshape(-1, 1)# train_y从 DataLoader 来,原始shape: (16,)一维:只有一个轴
1
2
3train_y = torch.tensor([1, 2, 3, 4, 5])
train_y.shape # torch.Size([5]) ← 这是1维,不是1行5列, 形状(5,)
train_y.ndim # 1二维:两个轴
1
2
3
4y_pred = torch.tensor([[1], [2], [3], [4], [5]])
y_pred.shape # torch.Size([5, 1]) ← 这是2维
y_pred.ndim # 2
y_pred = torch.tensor( [[1, 2, 3, 4, 5]] ) # 也是二维,形状(1,5)⭐⭐
[[1],[2],[3],[4],[5]]和[[1, 2, 3, 4, 5]]都是二维,他们的区别在哪里呢?

⭐⭐
[1, 2, 3, 4, 5]就是5个样本,零个特征吗?不是的。
[1, 2, 3, 4, 5]形状 (5,) 是 1维张量,它根本没有”样本”和”特征”的概念——就是一排数字(五个数排成一排)。由此可得
reshape(-1, 1)的作用:
把(16,) → (16, 1),变成16行1列的列向量。reshape前:[ 0.5, 1.2, 3.4, ... ]shape: (16,)
reshape后:[[ 0.5],
[ 1.2],
[ 3.4],
...]shape: (16, 1)
即:因为train_x是16个数据,每个数据有一个特征,所以也要把一开始仅仅是一行数据的train_y改成16个样本,每个样本有一个特征的形式
反向传播和优化器
optimizer.zero_grad():梯度清零
loss.backward():反向传播
optimizer.step():梯度更新
optimizer.zero_grad() 梯度清零函数
给出梯度的概念:梯度就是损失函数对每个参数的导数(偏导数)其实际意义为
“这个参数往哪个方向调一点点,loss 能下降得最快”
比如线性回归 y = wx + b,梯度就是 ∂loss/∂w 和 ∂loss/∂b,分别告诉你 w 和 b 该怎么调。
PyTorch 的设计哲学是:梯度累加给用户更多灵活性。比如你想手动模拟一个大 batch(显存不够时),就可以多次 backward() 让梯度自动累加,最后只 step() 一次,效果等价于用大 batch 训练。但代价是:如果你不做累加,就必须手动清零。下面演示累加是什么意思:
假设某个参数的梯度,第一个 batch 算出来是 0.3:
1 | loss.backward() |
第二个 batch 如果不清零,直接再 backward:
1 | loss.backward() |
然后 optimizer.step() 会用 0.8 去更新参数,而不是这个 batch 真正的梯度 0.5。
当显存不够、想用大 batch 但放不下时:
1 | optimizer.zero_grad() |
此为梯度累加技巧,用小显存模拟大 batch。所以累加这个特性不是 bug,是 feature,只是默认场景下需要手动清零而已。
loss.backward() 反向传播函数
反向传播是计算梯度的高效算法。
关键在于:神经网络是一个多层的复合函数,从输入到输出要经过很多步计算。要算 loss 对最前面参数的梯度,需要用链式法则一层层往回推。比如:
1 | 输入 x |
现在想求 ∂loss/∂w1,也就是 w1 对 loss 的影响有多大。
直接看不容易,但用链式法则从后往前拆:∂loss/∂w1 = ∂loss/∂z2 × ∂z2/∂z1 × ∂z1/∂w1,从 loss 出发,反向一层一层乘回去,就能算出最前面 w1 的梯度。这就是”反向传播”名字的由来——和前向计算方向相反。(复合函数求偏导)。PyTorch 在你做前向计算时,会偷偷记录一张计算图(记录每一步用了什么运算、依赖关系)。当你调用 loss.backward() 时,它就沿着这张图从后往前,用链式法则一次性算出所有参数的梯度,存到每个参数的 .grad 属性里。所以 loss.backward() 这一行代码,干的事情就是:从 loss 开始反向走 → 链式法则逐层求导 → 把结果存进每个参数的 .grad
Why在正向传播的时候把梯度算出来,非要加一个反向传播?
因为不知道目标是什么。前向传播时还没算出 loss,不知道最终误差是多少,自然没法算”误差对参数的梯度”。必须先走到终点拿到 loss,才能往回推。
想象开车从 A 到 B,路上经过很多路口:A → 路口1 → 路口2 → 路口3 → B。
前向传播:从 A 出发,一个路口一个路口往前开,最终到达 B,发现走偏了 10 公里
反向传播:从 B 往回看,”在路口3 我应该左转一点,在路口2 应该右转一点……”
前向的时候还不知道最终偏了多少,所以我们不知道每个路口该怎么修正。只有到了终点发现误差,才能往回推算每一步该负多少责任。
从公式上看:∂loss/∂w1 = ∂loss/∂z2 × ∂z2/∂z1 × ∂z1/∂w1, ∂z1/∂w1 前向时确实能算,但其中∂loss/∂z2 要等 loss 算出来才知道。中间的 ∂z2/∂z1 也要等 z2 算出来才知道。链条最后一环永远要等前向走完才能确定,所以没法真正”边走边算”,只能”走完再回头”。
optim.SGD() 优化器
optimizer = optim.SGD(model.parameters(), lr=0.01)
optim.SGD:选择 SGD 这个优化算法。SGD 全称是 Stochastic Gradient Descent(随机梯度下降),就是最基础的梯度下降法,每次用一个 batch 的数据算梯度然后更新参数。
model.parameters():告诉优化器”你要负责更新哪些参数”。这里把模型里所有的 weight 和 bias 都交给它管,之后 optimizer.step() 就会更新这些参数。
lr=0.01:学习率设为 0.01,控制每次参数更新的步子大小。0.01 意味着每次只走梯度值的 1%,步子比较小,更新比较稳。
optimizer.step()梯度更新函数
拿着反向传播算出来的梯度,按公式修改参数值,让 loss 变小。对于每个参数:新参数 = 旧参数 - 学习率 × 梯度(梯度指向的是 loss 增大的方向,而我们要让 loss 减小,所以要反着走(减去梯度)。这叫梯度下降。)即optimizer.step() 这一行代码,帮你对所有参数执行了下面的操作。
1 | model.weight = model.weight - lr * model.weight.grad |
损失函数、反向传播和优化器的关系
不同的损失函数会形成不同的 loss 曲面(可以想象成一个地形)。有的地形像一个光滑的碗,梯度下降能很快滑到最低点;有的地形坑坑洼洼,梯度下降容易卡在小坑里出不来。
好的损失函数:曲面光滑、碗状,梯度明确指向最低点
差的损失函数:曲面崎岖、有多个局部最小值,容易卡住
比如均方误差(MSE)对回归问题来说就是一个很好的光滑碗,所以回归任务默认用它。
反向传播给出下一步前进方向:比如站在曲面上的某个位置,反向传播看了一下周围,说”往这边走是下坡”。但不知道最低点在哪、离多远。你迈了一步,到了新位置,它再看一次,说”现在往那边走”,就这样一步一步挪。
优化器决定如何到达反向传播给出的下一步的方向,优化器选得好能少走弯路、少震荡、更快收敛。比如我们这基础 SGD 就是老老实实按
新参数 = 旧参数 - lr × 梯度,但它有几个问题:
- 遇到平坦区域,梯度很小,走得极慢
- 遇到陡峭区域,梯度很大,容易震荡甚至发散
- 遇到狭长的山谷地形,会在两侧来回弹,迟迟到不了最低点
所以还会有其他的优化器,比如Momentum(带惯性)和 Adam(最常用)
代码
创建数据集
1 | def create_dataset(): |
输出x,y,coef含义如下如所示

模型训练
1 | def train(x, y, coef): |
训练结果:

What should we do?
我们在模型训练的过程中来调整超参数使这些数据拟合度更高。从而能对新的数据预测精准,我们在拿到一堆数据之后主要做:
训练过程前选定超参数
模型结构
(Linear / 多层网络)损失函数
(MSE)优化器和学习率
(Adam, lr=0.001)训练轮数(
epochs)批大小
(batch_size)
训练过程中程序自动执行,我们不介入
优化器自动调
weight和biasloss随之变化跑完设定的轮数后结束
训练后:看结果决定要不要调
第一轮:用
SGD,lr=0.01,训练 100 轮 →loss降不下来第二轮:换成
Adam,lr=0.001,训练 500 轮 →loss降得很好 ✓第三轮:再加一层网络,
lr=0.001,训练 500 轮 → 更好了 ✓
绘制曲线
1 | # 8.绘制损失曲线 |







