深度学习——神经网络训练的最底层逻辑
·
数据流:生成 → 打乱 → 分批。
计算图:前向 → 损失 → 反向。
优化 loop:初始化 → 迭代更新 → 收敛。
一、头文件
import torch
import matplotlib.pyplot as plt #画图
import random #随机
二、线性回归数据生成函数
def create_data(w,b,data_num): #生成数据
x = torch.normal(0,1,(data_num,len(w)))
y = torch.matmul(x,w)+b #matmul表示矩阵相乘
noise = torch.normal(0,0.01,y.shape) #噪声要加到y上
y += noise
return x,y
num=500
true_w=torch.tensor([8.1,2,2,4])
true_b=torch.tensor(1.1)
X,Y = create_data(true_w,true_b,num)
#生产的数据画图
plt.scatter(X[:,3],Y,1)
plt.show()
1. 数据生成函数:
def create_data(w,b,data_num): #生成数据
x = torch.normal(0,1,(data_num,len(w)))
y = torch.matmul(x,w)+b #matmul表示矩阵相乘
noise = torch.normal(0,0.01,y.shape) #噪声要加到y上
y += noise
return x,y
|
|---|

2、参数设置:
num=500
true_w=torch.tensor([8.1,2,2,4])
true_b=torch.tensor(1.1)
参数详解
| 参数 | 值 | 含义 | 维度 |
|---|---|---|---|
num | 500 | 样本数量 | 标量 |
true_w | [8.1, 2, 2, 4] | 4 个特征的真实权重 | (4,) |
true_b | 1.1 | 真实偏置 | 标量 |
权重解读:
特征 1 权重:8.1 ← 影响最大
特征 2 权重:2.0
特征 3 权重:2.0
特征 4 权重:4.0 ← 影响次大
偏置:1.1
3、生成数据:
X,Y = create_data(true_w,true_b,num)
4、数据形状验证:
plt.scatter(X[:,3],Y,1)
plt.show()
参数详解
| 代码 | 含义 | 说明 |
|---|---|---|
X[:,3] | 第 4 个特征的所有样本值 | 形状 (500,) |
Y | 所有标签值 | 形状 (500,) |
1 | 散点大小 | 点的大小为 1 像素 |
为什么选 X[:,3]?
- 索引从 0 开始,
[:,3]表示第 4 个特征 - 第 4 个特征权重为 4.0,影响较大,散点图线性关系更明显
| 特征 | 权重 | 可视化效果 |
|---|---|---|
| 特征 1 | 8.1 | 斜率最大,关系最明显 |
| 特征 2 | 2.0 | 斜率较小 |
| 特征 3 | 2.0 | 斜率较小 |
| 特征 4 | 4.0 | 斜率适中,适合展示 |
5、完整的维度转换图:
┌─────────────────────────────────────────────────────────┐
│ 输入参数 │
│ true_w: torch.tensor([8.1, 2, 2, 4]) shape: (4,) │
│ true_b: torch.tensor(1.1) shape: () │
│ num: 500 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 生成特征矩阵 X │
│ X = torch.normal(0, 1, (500, 4)) │
│ X.shape = (500, 4) │
│ ┌──────────────────────────────┐ │
│ │ 特征 1 特征 2 特征 3 特征 4 │ │
│ │ x11 x12 x13 x14 │ ← 样本 1 │
│ │ x21 x22 x23 x24 │ ← 样本 2 │
│ │ ... ... ... ... │ │
│ │ x500,1 x500,2 x500,3 x500,4│ ← 样本 500 │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 计算线性关系 │
│ y = X @ w + b │
│ y[i] = 8.1*X[i,0] + 2*X[i,1] + 2*X[i,2] + 4*X[i,3] + 1.1│
│ y.shape = (500,) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 添加噪声 │
│ noise ~ N(0, 0.01²) │
│ y = y + noise │
│ y.shape = (500,) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 可视化 │
│ plt.scatter(X[:,3], Y, 1) │
│ 横轴:第 4 个特征 (权重=4.0) │
│ 纵轴:标签 Y │
│ 预期斜率 ≈ 4.0 │
└─────────────────────────────────────────────────────────┘
6、核心知识点总结:
┌─────────────────────────────────────────────────────────┐
│ 核心知识点 │
├─────────────────────────────────────────────────────────┤
│ 1. torch.normal() - 正态分布采样 │
│ 2. torch.matmul() - 矩阵乘法 │
│ 3. 广播机制 - 标量 + 向量 │
│ 4. 张量索引 - X[:,3] 选取特定特征 │
│ 5. 线性回归模型 - y = Xw + b + ε │
│ 6. 数据可视化 - matplotlib 散点图 │
│ 7. 合成数据 - 已知真实参数的监督学习数据 │
└─────────────────────────────────────────────────────────┘
三、取数据函数
def data_provider(data,label,batchsize): #每次访问这个函数,都能提供一批数据
length = len(label)
indices = list(range(length))
#我不能按顺序取,需把数据打乱
random.shuffle(indices)
for each in range(0,length,batchsize):
get_indices = indices[each:each+batchsize]
get_data = data[get_indices]
get_label = label[get_indices]
yield get_data,get_label #有存档点的return
batchsize = 16
#打印取得的数据
# for batch_x,batch_y in data_provider(X,Y,batchsize):
# print(batch_x,batch_y)
# break
1、代码功能总览:
在典型的深度学习训练循环中,这段代码位于数据加载阶段。它实现了随机小批量梯度下降(Stochastic Mini-batch Gradient Descent) 所需的数据流控制,是连接数据集与模型训练的桥梁。
| 功能模块 | 代码实现 | 目的 |
|---|---|---|
| 数据打乱 | random.shuffle(indices) | 打破样本间的相关性,防止模型记忆顺序 |
| 分批处理 | range(0, length, batchsize) | 将大数据集切分为小块,适应显存限制 |
| 惰性加载 | yield | 生成器模式,节省内存,不用一次性加载所有数据 |
| 索引提取 | data[get_indices] | 利用高级索引获取对应的数据块 |
2、逐行深度解析
1. 函数定义
def data_provider(data, label, batchsize):
data: 输入特征矩阵(通常是 PyTorch Tensor 或 NumPy 数组),形状如(总样本数,特征数)。label: 标签向量(通常是 PyTorch Tensor 或 NumPy 数组),形状如(总样本数,)。batchsize: 批量大小,即每次训练迭代使用的样本数量(例如 16, 32, 64)。- 注释:
#每次访问这个函数,都能提供一批数据—— 这描述的是生成器的特性。
2. 获取长度与创建索引
length = len(label)
indices = list(range(length))
length: 获取数据集的总样本数。- 注意:通常
len(data)和len(label)应该相等。
- 注意:通常
indices: 创建一个顺序列表[0, 1, 2, ..., length-1]。- 作用:我们不直接打乱
data本身(因为复制大数据很耗内存),而是打乱索引,通过索引去取数据。
- 作用:我们不直接打乱
3. 打乱索引(Shuffle)
#我不能按顺序取,需把数据打乱
random.shuffle(indices)
random.shuffle: 原地打乱列表顺序。- 为什么要打乱?
- 打破相关性:如果数据是按类别排序的(如前 500 个是猫,后 500 个是狗),不打乱会导致模型在一个 epoch 内只学到一种类别。
- 加速收敛:随机梯度下降(SGD)需要样本的随机性来跳出局部最优解。
- 评估泛化性:确保每个 batch 的数据分布接近整体分布。
4. 循环分批
for each in range(0, length, batchsize):
range(0, length, batchsize): 生成起始索引序列。- 例如:
length=500,batchsize=16。 - 生成:
0, 16, 32, ..., 496。
- 例如:
each: 当前 batch 的起始位置。
5. 获取当前批次的索引
get_indices = indices[each:each+batchsize]
- 切片操作:从打乱后的
indices中截取一段。 - 边界处理:如果是最后一个 batch,
each+batchsize可能超过length。Python 切片会自动处理边界,只取到列表末尾,因此最后一个 batch 的大小可能小于 batchsize。
6. 提取数据与标签
get_data = data[get_indices]
get_label = label[get_indices]
- 高级索引(Fancy Indexing):
- 如果
data是 PyTorch Tensor 或 NumPy Array,这行代码有效。它会根据get_indices列表中的数字,提取对应的行。 - 例如:
get_indices = [5, 100, 3],则get_data包含原数据中第 5、100、3 行的数据。
- 如果
7. 生成器返回(Yield)
yield get_data, get_label #有存档点的 return
yieldvsreturn:这是本代码的核心。return:函数结束,返回值,下次调用从头开始。yield:函数暂停,返回值,保留当前状态(局部变量、循环位置)。下次调用时,从yield下一行继续执行。
- 好处:
- 节省内存:不需要一次性生成所有 batch 的列表,用多少生成多少。
- 流式处理:适合处理无法一次性装入内存的大数据集。
四、定义一个模型和loss
def fun(x,w,b):
pred_y = torch.matmul(x,w)+b
return pred_y
def maeLoss(pre_y,y):
return torch.sum(abs(pre_y-y))/len(y)
定义了一个线性回归模型的前向传播函数和一个平均绝对误差(MAE)损失函数。这是深度学习训练流程中两个最核心的组件:模型定义与损失计算。
1、代码功能总览
| 函数名 | 功能 | 对应数学概念 | 作用阶段 |
|---|---|---|---|
fun(x, w, b) | 模型预测 | 假设函数 h(x)=wx+b | 前向传播 (Forward) |
maeLoss(pre_y, y) | 计算损失 | 损失函数 两数相减的绝对值 | \hat{y} - y |
整体流程:输入数据 x → 通过 fun 得到预测值 pred_y → 通过 maeLoss 对比真实值 y 计算误差 → 用于反向传播更新参数。
2、函数 1:fun(x, w, b) 深度解析
def fun(x,w,b):
pred_y = torch.matmul(x,w)+b
return pred_y
1. 参数与维度
| 参数 | 含义 | 典型形状 (Shape) | 说明 |
|---|---|---|---|
x | 输入特征 | (batch_size, num_features) | 例如 (16, 4) 表示 16 个样本,4 个特征 |
w | 权重向量 | (num_features,) 或 (num_features, 1) | 例如 (4,) |
b | 偏置标量 | () 或 (1,) | 例如 1.0 |
2. 核心运算:torch.matmul(x, w) + b
torch.matmul(x, w):- 执行矩阵 - 向量乘法。
- 计算每个样本的加权和:i∑xi⋅wi 。
- 输出形状:
(batch_size,)。
+ b:- 利用 PyTorch 的广播机制 (Broadcasting)。
- 将标量
b加到结果向量的每一个元素上。
- 数学公式:

3、函数 2:maeLoss(pre_y, y) 深度解析
def maeLoss(pre_y,y):
return torch.sum(abs(pre_y-y))/len(y)
1. 功能定义
- 这是 Mean Absolute Error (MAE),也称为 L1 Loss。
- 它计算预测值与真实值之间绝对误差的平均值。
2. 逐步拆解
| 代码片段 | 含义 | 数学表达 | 形状变化 |
|---|---|---|---|
pre_y - y | 计算残差(误差) | y^i−yi | (batch_size,) |
abs(...) | 取绝对值 | $ | \hat{y}_i - y_i |
torch.sum(...) | 求和 | $\sum_{i=1}^{n} | \hat{y}_i - y_i |
/ len(y) | 求平均 | n1∑... | 标量 (1,) |
3. 数学公式
![]()
4. MAE 的特性(重要)
| 特性 | 说明 | 影响 |
|---|---|---|
| 鲁棒性 | 对**异常值(Outliers)**不敏感 | 如果数据中有个别极大的误差,MAE 不会像 MSE 那样被剧烈放大 |
| 梯度 | 梯度恒为 1 或 -1(除了 0 点) | 收敛速度稳定,但在接近最优解时可能震荡,不如 MSE 精细 |
| 可导性 | 在 0 处不可导(次梯度) | PyTorch 的 autograd 会自动处理次梯度,通常不影响训练 |
五、随机梯度下降
def sgd(paras,lr): #随机梯度下降,更新参数
with torch.no_grad(): #属于这句代码的部分不计算梯度
for para in paras:
para -= para.grad*lr #不能写成para=para-para.grad*lr
para.grad.zero_() #使用过的梯度归零
PyTorch 训练的三个黄金法则:
- 更新时要闭眼:
with torch.no_grad(),更新参数时不要记录计算图。 - 修改要原地:
para -=,确保模型持有的参数引用被实际修改。 - 梯度要清零:
para.grad.zero_(),防止梯度累加污染下一次迭代。
1、代码功能总览
| 函数名 | 功能 | 输入 | 输出 | 阶段 |
|---|---|---|---|---|
sgd | 手动优化器 | paras: 参数列表 [w, b]<br>lr: 学习率 | 无(原地修改参数) | 反向传播后 |
数学原理: 
2、逐行深度解析
第 1 行:函数定义
def sgd(paras,lr): #随机梯度下降,更新参数
paras: 一个包含所有需要更新的参数的列表。- 通常形式:
[w, b]或net.parameters()。 - 这些参数必须是
requires_grad=True的张量。
- 通常形式:
lr: 学习率(Learning Rate)。- 控制每次更新的步长。
- 太大:可能越过最优解,导致发散。
- 太小:收敛速度极慢。
第 2 行:停止梯度追踪
with torch.no_grad(): #属于这句代码的部分不计算梯度
- 核心作用:临时禁用 PyTorch 的自动求导引擎(Autograd)。
- 为什么要这样做?
- 节省内存:参数更新操作(减法和乘法)不需要计算梯度。如果不禁用,PyTorch 会尝试构建更新操作的计算图,消耗大量显存。
- 防止图无限增长:如果不禁用,更新操作会被记录为计算图的一部分。下一个 epoch 的
backward()会沿着这个图一直回传到初始状态,导致计算图无限变大,最终内存溢出(OOM)。 - 语义明确:我们只关心 Loss 对 参数 的梯度,不关心 新参数 对 旧参数 的梯度。
第 4 行:参数更新(关键!)
para -= para.grad*lr #不能写成 para=para-para.grad*lr
- 数学含义:

para.grad: 存储了上一次loss.backward()计算出的梯度。-=(原地操作) vs=(赋值操作):- ✅
para -= ...(正确):- 这是原地修改(In-place Operation)。
- 它直接修改了
para指向的内存数据。 - 因为模型(Model)内部持有的也是这个内存地址的引用,所以模型中的参数同步被更新了。
- ❌
para = ...(错误):- 这是变量重绑定(Rebinding)。
para - ...计算出一个新张量。para =将局部变量para指向了这个新张量。- 后果:模型内部持有的旧张量没有被修改。下一次前向传播时,模型依然使用旧的参数,训练无效。
- 注释解读:代码中的注释
#不能写成 para=para-para.grad*lr非常精准,指出了新手最容易犯的错误。
- ✅
第 5 行:梯度清零
para.grad.zero_() #使用过的梯度归零
- 核心作用:将参数的
.grad属性重置为 0。 - 为什么要清零?
- PyTorch 的梯度默认是**累加(Accumulate)**的。
- 如果不清零,下一次
loss.backward()计算出的梯度会加到现有的梯度上。 - 后果:梯度会越来越大,参数更新方向错误,模型无法收敛。
- 场景:每个 batch 更新一次参数,所以每个 batch 后必须清零。
六、训练
lr=0.03
w_0=torch.normal(0,0.01,true_w.shape,requires_grad=True) #这个w需要计算梯度
b_0=torch.tensor(0.01,requires_grad=True)
print(w_0,b_0)
epochs=50 #训练的轮数
for epoch in range(epochs):
data_loss=0
for batch_x,batch_y in data_provider(X,Y,batchsize):
pred_y = fun(batch_x,w_0,b_0)
loss = maeLoss(pred_y,batch_y)
loss.backward()
sgd([w_0,b_0],lr)
data_loss += loss
print("epoch %03d:loss:%.6f"%(epoch,data_loss)) #打印每一轮的loss,方便查看
print("真实的函数值是:",true_w,true_b)
print("训练得到的参数值是:",w_0,b_0)
通过迭代优化,让初始化的随机参数 w_0, b_0 逐渐逼近真实的参数 true_w, true_b。
1、代码功能总览
| 代码块 | 功能 | 关键操作 |
|---|---|---|
| 参数初始化 | 设置模型初始状态 | torch.normal, requires_grad=True |
| 训练循环 | 执行迭代优化 | 外层 Epoch,内层 Batch |
| 核心四步 | 完成一次参数更新 | 前向 → 损失 → 反向 → 更新 |
| 结果验证 | 评估训练效果 | 对比 真实值 vs 训练值 |
2、逐行深度解析
1. 参数初始化
lr=0.03
w_0=torch.normal(0,0.01,true_w.shape,requires_grad=True) #这个 w 需要计算梯度
b_0=torch.tensor(0.01,requires_grad=True)
print(w_0,b_0)
lr=0.03: 学习率。控制参数更新的步长。- 太大:损失函数震荡,不收敛。
- 太小:训练速度极慢。
- 0.03 是经验值,针对此数据集通常有效。
w_0初始化:torch.normal(0, 0.01, ...): 从均值为 0,标准差为 0.01 的正态分布初始化。- 为什么要随机初始化? 如果全初始化为 0,所有神经元对称,无法学习不同特征。
requires_grad=True: 关键标志。告诉 PyTorch 追踪对这个张量的所有操作,以便后续计算梯度。没有它,backward()无效。
b_0初始化: 初始化为 0.01 的标量,同样需要梯度。print: 打印初始参数,用于对比训练后的变化。
2. 训练循环结构
epochs=50 #训练的轮数
for epoch in range(epochs):
data_loss=0
for batch_x,batch_y in data_provider(X,Y,batchsize):
# ... 训练步骤 ...
epochs=50: 轮数。表示将整个数据集遍历 50 次。- 太少:模型欠拟合(没学好)。
- 太多:模型过拟合(死记硬背)且浪费时间。
- 外层循环 (
epoch): 控制训练的整体进度。 - 内层循环 (
batch): 通过data_provider每次获取一个小批量数据。- 这是**随机梯度下降(SGD)**的体现:每次只用一部分数据更新参数,而不是全部。
data_loss=0: 用于累计当前 epoch 的总损失,以便评估该轮训练的整体效果。
3. 核心训练四步走
pred_y = fun(batch_x,w_0,b_0) # 1. 前向传播
loss = maeLoss(pred_y,batch_y) # 2. 计算损失
loss.backward() # 3. 反向传播
sgd([w_0,b_0],lr) # 4. 参数更新
data_loss += loss # 5. 记录损失
这是深度学习的标准范式,每一步都至关重要:
- 前向传播 (
fun): 用当前参数计算预测值 y^ 。 - 计算损失 (
maeLoss): 衡量预测值 y^ 与真实值 y 的差距。 - 反向传播 (
loss.backward()): 自动计算损失函数对每个参数 (w_0,b_0) 的梯度,并存入.grad属性。 - 参数更新 (
sgd): 利用梯度更新参数(w = w - lr * grad),并清零梯度。 - 记录损失: 累加 loss 用于监控收敛情况。
4. 监控与打印
print("epoch %03d:loss:%.6f"%(epoch,data_loss)) #打印每一轮的 loss,方便查看
%03d: epoch 编号占 3 位,不足补 0(如 001, 050)。%.6f: 损失值保留 6 位小数。- 目的: 观察
loss是否随 epoch 增加而逐渐下降。如果下降,说明模型正在学习;如果不变或上升,说明学习率或模型有问题。 - ⚠️ 潜在问题:
data_loss是张量累加的结果。最好使用data_loss += loss.item()将其转换为 Python 浮点数,否则打印时会显示tensor(...)且可能占用额外内存。
5. 结果验证
print("真实的函数值是:",true_w,true_b)
print("训练得到的参数值是:",w_0,b_0)
- 目的: 因为数据是合成的(我们知道
true_w),所以可以直接对比。 - 预期结果: 训练后的
w_0应非常接近true_w(例如[8.1, 2, 2, 4]),b_0接近1.1。 - 意义: 验证算法的正确性。如果在真实场景中,我们没有
true_w,只能通过测试集损失来评估。
七、可视化
idx = 3 #0-3
plt.plot(X[:,idx].detach().numpy(),X[:,idx].detach().numpy()*w_0[idx].detach().numpy()+b_0.detach().numpy())
plt.scatter(X[:,idx],Y,1)
plt.show()
用于可视化训练后的线性回归模型,将学习到的线性关系与实际数据点绘制在同一张图上,直观展示模型的拟合效果。
1、代码功能总览
| 代码行 | 功能 | 输出 |
|---|---|---|
idx = 3 | 选择第 4 个特征进行可视化 | 标量 3 |
plt.plot(...) | 绘制模型预测的直线 | 回归线 |
plt.scatter(...) | 绘制实际数据点 | 散点图 |
plt.show() | 显示图像 | 可视化窗口 |
核心目的:验证训练后的参数 w_0, b_0 是否能正确描述特征与标签之间的关系。
2、逐行深度解析
第 1 行:选择特征索引
idx = 3
- 含义:选择第 4 个特征(索引从 0 开始:0, 1, 2, 3)。
- 为什么选第 4 个特征?
- 回顾真实权重:
true_w = [8.1, 2, 2, 4] - 第 4 个特征权重为
4.0,影响较大,线性关系明显,适合展示。 - 第 1 个特征权重
8.1更大,但可能斜率太陡,可视化效果不如第 4 个特征适中。
- 回顾真实权重:
第 2 行:绘制模型预测线
plt.plot(X[:,idx].detach().numpy(), X[:,idx].detach().numpy()*w_0[idx].detach().numpy()+b_0.detach().numpy())
这是本代码的核心,绘制的是训练后模型学到的线性关系。
1. 结构拆解
| 部分 | 代码 | 含义 | 形状 |
|---|---|---|---|
| X 轴 | X[:,idx].detach().numpy() | 第 4 个特征的所有样本值 | (500,) |
| 斜率 | w_0[idx].detach().numpy() | 第 4 个特征的学习权重 | 标量 |
| 截距 | b_0.detach().numpy() | 学习到的偏置 | 标量 |
| Y 轴 | X[:,idx] * w_0[idx] + b_0 | 模型预测值(仅考虑该特征) | (500,) |
2. 为什么要用 .detach() 和 .numpy()?
| 方法 | 作用 | 必要性 |
|---|---|---|
.detach() | 从计算图中分离张量,停止梯度追踪 | ✅ 必须。matplotlib 无法处理需要梯度的张量 |
.numpy() | 将 PyTorch 张量转换为 NumPy 数组 | ✅ 必须。matplotlib 只接受 NumPy 数组或列表 |
转换流程:
PyTorch Tensor (requires_grad=True)
↓ .detach()
PyTorch Tensor (requires_grad=False)
↓ .numpy()
NumPy Array
↓
matplotlib 可以绘制
3. 数学公式
绘制的直线方程为: ![]()
注意:这是一个简化视图。真实模型是:
由于其他特征的存在,散点不会完美落在直线上,但应呈现线性趋势。
第 3 行:绘制实际数据点
plt.scatter(X[:,idx], Y, 1)
| 参数 | 含义 | 说明 |
|---|---|---|
X[:,idx] | X 轴数据 | 第 4 个特征的值(PyTorch 张量可直接用于 scatter) |
Y | Y 轴数据 | 真实标签值 |
1 | 点的大小 | s=1,点非常小,适合密集数据 |
⚠️ 潜在问题:
X[:,idx]是 PyTorch 张量,某些版本的matplotlib可能警告或报错。- 建议:统一转换为 NumPy,
plt.scatter(X[:,idx].numpy(), Y.numpy(), 1)。
第 4 行:显示图像
plt.show()
- 渲染并显示图像窗口。
- 在 Jupyter Notebook 中会直接内嵌显示。
- 在脚本中会弹出独立窗口。
3、核心知识点总结
┌─────────────────────────────────────────────────────────┐
│ 核心知识点 │
├─────────────────────────────────────────────────────────┤
│ 1. .detach() - 从计算图分离张量 │
│ 2. .numpy() - PyTorch 转 NumPy │
│ 3. plt.plot() - 绘制连续曲线(直线) │
│ 4. plt.scatter() - 绘制离散点 │
│ 5. 张量索引 - X[:,idx] 选取特定特征列 │
│ 6. 模型可视化 - 验证训练效果的重要手段 │
│ 7. 单特征视图 - 高维模型的降维展示技巧 │
└─────────────────────────────────────────────────────────┘
更多推荐
所有评论(0)