1.Tensor(张量)
1.1 Tensor 是什么?
Tensor = N 维数组(支持 GPU + 自动求梯度 + 更高性能)
标量(0 维)→ 3
向量(1 维)→ [1,2,3]
矩阵(2 维)→ [[1,2],[3,4]]
三维张量(3 维)→ 图像 RGB
四维张量(4 维)→ batch 图像(比如 (64,3,32,32))
1.2 创建张量
# 简单张量
a = torch.tensor([1, 2, 3])
# 创建全 0
torch.zeros((3, 4))
# 创建全 1
torch.ones((2, 3))
# 创建随机张量
torch.randn((3, 4))1.3 查看 Tensor 信息
# 查看形状
a.shape
# 查看维度
a.ndim
# 查看数据类型
a.dtype1.4 Tensor 的数学运算
# 加减乘
a+b a-b a*b
# 矩阵运算
a@b
# 广播
a + 31.5 Tensor 的切片
# 1d张量
a = torch.arange(10)
print(a[2:7])
# nd张量
x = torch.randn((3,4))
print(x[:, 1])1.6 CPU 与 GPU 切换
# 查看gpu 是否可用
torch.cuda.is_available()
# 将tensor 放在gpu 上
a = torch.ones((3,3)).cuda()
# 从gpu 转到 cpu
a = a.cpu()
# 常用写法
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
a = a.to(device)1.7 Tensor 和 Numpy 转换
# tensor -> numpy
a.numpy()
# numpy -> tensor
torch.from_numpy(np_array)
# tensor.from_numpy() 产生的 tensor 和 numpy arrary 共享内存(改变一个,另一个也跟着转变)
# 如果不想共享内存 则使用
torch.tensor(b)2.Autograd(自动求导)
Autograd 负责自动构建计算图,并根据前向计算自动推导出梯度。
深度学习训练的流程(本质)
前向传播:算 output
反向传播:算梯度(autograd 自动完成)
梯度更新:optimizer.step()
手动求导
# 方法一:直接展开(代数法)
# 1.展开平方
out = (5x+3)²
= (5x+3) × (5x+3) # 平方就是乘以自身
= 5x × (5x+3) + 3 × (5x+3) # 分配律
= 25x² + 15x + 15x + 9 # 进一步展开
= 25x² + 30x + 9 # 合并同类项
# 2.逐项求导
d(out)/dx = d(25x²)/dx + d(30x)/dx + d(9)/dx
= 25 × d(x²)/dx + 30 × d(x)/dx + 0
= 25 × (2x) + 30 × 1
= 50x + 30
# 链式法则(复合函数求导)
# 1.识别复合结构
外层函数:f(u) = u² # 平方函数
内层函数:u(x) = 5x + 3 # 线性函数
复合函数:f(u(x)) = (5x+3)²
# 2.计算各层导数
1. 外层导数:df/du = 2u
2. 内层导数:du/dx = 5
# 3.应用链式法则
链式法则公式:df/dx = df/du × du/dx
代入:
df/dx = (2u) × (5)
= 2u × 5
= 10u
= 10 × (5x+3) # 将 u = 5x+3 代入
= 50x + 302.1 requires_grad
# 你希望 PyTorch 帮你计算梯度,就要给 Tensor 设置:requires_grad=True
import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)2.2 计算图是如何构建的
x = torch.tensor([2.0], requires_grad=True)
y = x * 3
z = y ** 2
"""
这里背后发生了什么:
PyTorch 记录了:
z 是通过 y → (平方运算) 产生
y 是通过 x → (乘法运算) 产生
计算图为:
x --(*3)--> y --(^2)--> z
"""2.3 backward() 自动反向传播
# 对 z 求梯度:
z.backward()
# PyTorch 会自动计算:
dz/dx = dz/dy * dy/dx
dz/dx = (2y) * (3)
# backward() 会把梯度写入 x.grad2.4 常见规则
2.4.1 .grad 只会被写入 叶子节点(leaf tensor)
x = torch.tensor([3.0], requires_grad=True)
y = x * 2
z = y ** 2只有 x.grad 会有值
y.grad 和 z.grad 没有梯度(默认不存)
2.4.2 每次 backward() 前要梯度清零
因为
.grad会累加x.grad = None or optimizer.zero_grad()
2.4.3 backward() 只能对标量调用
标量:一个单独的数,只有大小 没有方向
2.4.4 线性回归
import torch
# 训练数据
x = torch.randn((10,1))
y = 3 * x + 1
# 参数(需要求梯度)
w = torch.randn((1,1), requires_grad=True)
b = torch.randn((1), requires_grad=True)
lr = 0.1
for i in range(100):
# forward
y_pred = x @ w + b
# loss
loss = ((y_pred - y)**2).mean()
# backward
loss.backward()
# 梯度下降:w = w - lr * dw
with torch.no_grad():
w -= lr * w.grad
b -= lr * b.grad
w.grad = None
b.grad = None
print("w =", w)
print("b =", b)3.广播、常见维度操作、拼接与切分、矩阵运算
3.1 广播机制 Broadcasting
PyTorch 在计算时,如果两个张量维度不一样,它会自动扩展它们,使形状兼容。
广播规则类似 numpy:
从后往前对齐维度
如果一个维度是 1,可以扩展
如果维度不相等且都不是 1 → 报错
import torch
a = torch.randn(3, 1) # shape (3,1)
b = torch.randn(1, 4) # shape (1,4)
c = a + b # 自动广播成为 (3,4)
print(c.shape)3.2 常见维度操作
3.2.1 reshape / view(改变形状)
a = torch.arange(12)
b = a.reshape(3, 4)
# view 更严格,要求内存连续。3.2.2 unsqueeze:增加某一维 —— squeeze:去掉维度为1的维度
# squeeze : 挤压
# 加上un 取反
# unsqueeze : 解压 或是拓展
a = torch.randn(1,3,1,5)
b = a.squeeze() # 去掉所有 1 维度
a = torch.tensor([1,2,3])
print(a.shape) # (3,)
b = a.unsqueeze(0) # (1,3)
c = a.unsqueeze(1) # (3,1)3.2.3 permute:改变维度顺序
x = torch.randn(2, 3, 4)
y = x.permute(0, 2, 1) # (2,4,3) 中间的参数为 维度的下标3.2.4 transpose:两维度交换
x = torch.randn(3, 4)
y = x.transpose(0, 1) # 变成 (4,3)
# 两个参数代表维度的索引 指要交换的两个维度3.2.5 expand / repeat(复制维度)
expand
核心机制:基于广播机制,只能将维度大小为1的维度扩展到更大的尺寸。
内存:与原始张量共享内存(是原张量的视图)。
变化:如果你修改了原始张量,expand后的张量也会变化,反之亦然(因为它们共享内存)。
维度限制:只能扩展维度大小为1的维度,不能扩展其他维度(比如维度大小为2的维度不能直接扩展为5)。
repeat
核心机制:通过复制数据来扩展张量,可以扩展任何维度(无论原始维度大小是多少)。
内存:不共享内存,创建新的张量,数据被复制。
变化:修改原始张量不会影响repeat得到的张量,修改repeat得到的张量也不会影响原始张量。
维度灵活性:可以任意指定每个维度重复的次数。
# 原始张量
x = torch.tensor([[1, 2, 3]]) # shape: (1, 3)
# expand
y_expand = x.expand(4, 3) # 将第0维从1扩展到4
# 可以这样理解:y_expand是x的"视图",逻辑上有4行,但实际存储只有1行数据
# repeat
y_repeat = x.repeat(4, 1) # 第0维重复4次,第1维重复1次
# 可以这样理解:y_repeat是真正复制了4份数据,存储了4行数据
# 内存共享测试
print("原始x的内存地址:", x.storage().data_ptr())
print("expand后的内存地址:", y_expand.storage().data_ptr())
print("repeat后的内存地址:", y_repeat.storage().data_ptr())
# 你会发现expand和x相同,repeat不同
# 修改测试
x[0, 0] = 100
print("修改x后,y_expand[0, 0]:", y_expand[0, 0]) # 变成100
print("修改x后,y_repeat[0, 0]:", y_repeat[0, 0]) # 仍然是13.3 拼接与切分
3.3.1 cat:拼接已有维度
a = torch.ones(2,3)
b = torch.zeros(2,3)
c = torch.cat([a,b], dim=0) # (4,3) 下标为0的维度相加
d = torch.cat([a,b], dim=1) # (2,6) 下标为1的维度相加3.3.2 stack:会创建新维度
a = torch.ones(3)
b = torch.zeros(3)
torch.stack([a,b], dim=0) # (2,3)
torch.stack([a,b], dim=1) # (3,2)3.3.3 split & chunk:切分张量
x = torch.arange(10)
print(torch.split(x, 3)) # 按大小切 [3,3,3,1]
print(torch.chunk(x, 3)) # 均匀切 3 份 [4,3,3]3.4 矩阵运算
3.4.1 matmul:自动选择正确的矩阵乘法
a = torch.randn(3, 4)
b = torch.randn(4, 5)
c = a @ b # 等价 matmul3.4.2 mm:两个 2D 矩阵
# 只能对2d张量相乘,第一个张量的列 必须要等与 第二个张量的行
c = torch.mm(a, b)3.4.3 bmm:批矩阵乘法
# 只能对3d张量相乘,第一个张量的最后一个维度 必须要等于 第二个张量的 倒数第二个维度
a = torch.randn(10, 3, 4)
b = torch.randn(10, 4, 5)
c = torch.bmm(a, b) # (10,3,5)4.计算图、Autograd机制、requires_grad意义、detach 与 torch.no_grad 的区别、梯度清零的深层原因
4.1 计算图(Computational Graph)
PyTorch 会在你执行操作时,动态构建一个有向图:
节点:Tensor
边:操作(如 +, matmul, relu, sigmoid, mean)
x = torch.tensor([2.0], requires_grad=True)
y = x * 3
z = y + 5
out = z ** 2
# 计算图
x → (*) → y → (+) → z → (**) → out每一步都会产生一个新 Tensor,它记录:
是由哪种操作产生的:
Tensor.grad_fn输入是谁(也就是“父节点”)
4.2 backward() 做了什么?
# 代表给 out 注入一个梯度 1(d(out)/d(out) = 1)
out.backward()然后 PyTorch 会沿计算图反向传播,逐步计算:
d(out)/d(z)
d(out)/d(y)
d(out)/d(x)
最终存储在 x.grad;这就是自动求导的核心
4.3 requires_grad = True 的意义
Tensor 是否参与反向传播取决于:
x = torch.tensor([1.0], requires_grad=True)
如果 True → PyTorch 跟踪它
如果 False → 认为它是常量,不计算梯度
4.4 detach vs no_grad
4.4.1 detach()
从计算图中切断一个 Tensor 不影响其他人,只影响自己。
y = x * 3
z = y.detach()
z和y数值一样,但:z不会被追踪梯度y仍然会被追踪
适用于:
做 forward 但不想反向传播到某一层
RNN 训练中清空历史图
4.4.2 with torch.no_grad()
临时关闭整个当前 block 的梯度追踪
with torch.no_grad():
w -= lr * w.grad常用于:
权重更新(optimizer step)
推理阶段(不训练)
节省显存
4.5 为什么必须梯度清零 zero_grad()?
因为梯度会累加!
每次 backward() 都会把梯度累加到 .grad 上
w.grad += 当前梯度
因此在每个 batch 前必须:
w.grad = None
# 或
w.grad.zero_()4.6 手写线性回归,最好全部理解
import torch
# 机器学习通常要求输入为二维,每行是一个样本。
x = torch.linspace(0,10,20).unsqueeze(1)
# 创建标准答案 让模型学习的目标规律
y = x * 3 + 2 # 3 代表预设的 权重 2 代表 预设的 偏置
# 设置可学习参数 w 和 b
# w 称为 权重或斜率,表示x每变化一个单位,y会变化多少
w = torch.randn(1,requires_grad=True) # 开启梯度追踪
# b 称为 偏置或截距,表示当x为0时y的值。
b = torch.randn(1,requires_grad=True)
# 学习率:控制每次参数更新的步长。太大容易“跨过”最优点,太小则学习太慢。0.01是一个常用值
lr =0.01
# 一个epoch代表着一次完整的数据训练
for epoch in range(200):
# 使用当前的 权重 和 偏置 计算模型的 预测值
y_pred = x * w + b
# 计算损失 衡量预测值与真实值差距,平方可放大大误差并确保正值。
loss = ((y_pred - y) ** 2).mean()
# 反向传播,自动计算梯度 PyTorch沿计算图反向计算loss对w和b的导数
loss.backward()
# 关闭梯度上下文,确保更新参数时不记录梯度,避免干扰到下次计算
"""
为什么必须要关闭梯度上下文
1.避免梯度被跟踪导致计算图不断变大
2.避免 w和b 的更新操作参与到梯度计算中
"""
with torch.no_grad():
# 沿梯度反方向调整参数,使loss减小
# loss 的 越来越小,那么梯度也会越来越小
w -= lr * w.grad
b -= lr * b.grad
# PyTorch梯度会累积,必须手动清零防止影响下次迭代。
# backward() 是累加梯度,如果不清零,会让多次训练的梯度叠加,导致优化方向错误。
w.grad.zero_()
b.grad.zero_()
if epoch % 10 == 0:
print(f"epoch {epoch}, loss={loss.item():.4f}, w={w.item():.4f}, b={b.item():.4f}")5.神经网络(nn.Module + nn.Linear)
模型训练完整流程:
使用 MSE loss 和 SGD 优化器
import torch
from torch import nn
# 所有模型都必须继承 nn.Module
class my_model(nn.Module):
def __init__(self):
super().__init__()
# 定义网络层结构
# fc1: 输入1个特征 → 输出10个特征(10个神经元) nn.Linear 他在内部自动创建了 w权重 b偏置
# 并且默认带有 requires_grad=True
self.fc1 = nn.Linear(in_features=1, out_features=10)
# ReLU激活函数(非线性变换)
self.relu = nn.ReLU()
# fc2: 输入10个特征 → 输出1个特征
self.fc2 = nn.Linear(in_features=10, out_features=1)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
model = my_model()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# to(device) 使用GPU进行训练
x = torch.tensor([[1.0],[2.0],[3.0]]).to(device).to(device)
y = torch.tensor([[2.0],[4.0],[6.0]]).to(device).to(device)
criterion = nn.MSELoss().to(device) # 损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=0.01) # 优化器
for epoch in range(200):
# 1. forward 调用 model(x)就相当于调用了 forward方法 -> forward 是定义前向计算逻辑
pred = model(x)
# 2. 计算损失
loss = criterion(pred, y)
# 3. backward
optimizer.zero_grad() # 清零梯度
loss.view()
# 4. 更新参数
optimizer.step()
if epoch % 40 == 0:
print(epoch, loss.item())
learned_weight = model.fc1.weight.data
learned_bias = model.fc1.bias.data
print(learned_weight, learned_bias)标准流程:forward(计算图) -> loss(损失值) -> zero_grad(清空梯度) -> backward(反向传播) -> step(更新参数)
核心区别对比:
6.Dataset & DataLoader
手喂数据的坏处:
数据量大时无法全部放内存
每次都整批训练 → 不稳定
没有打乱数据 → 容易过拟合
不能动态扩展数据源(图片/文本/数据库)
import torch
from torch.utils.data import Dataset, DataLoader
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 自定义数据集
class LinearDataset(Dataset):
def __init__(self):
# 100个样本
self.x = torch.linspace(0, 10, 100).unsqueeze(1).to(device)
self.y = 2 * self.x + 1
def __len__(self):
return len(self.x)
def __getitem__(self, idx):
return self.x[idx], self.y[idx]
# 实例化数据集 & 数据加载器
dataset = LinearDataset()
loader = DataLoader(dataset, batch_size=10, shuffle=True)
# 简单模型
model = torch.nn.Linear(1, 1).to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
criterion = torch.nn.MSELoss().to(device)
# 训练
for epoch in range(5):
for x, y in loader:
batch_x = x.to(device)
batch_y = y.to(device)
pred = model(batch_x)
loss = criterion(pred, batch_y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"Epoch {epoch}: loss={loss.item():.4f}")使用batch(mini-batch)的好处
Mini-Batch 可以在内存可控范围内,使用 GPU 高效并行,并让梯度既有稳定性又保留随机性(提高收敛质量)
训练完成后取预测结果
x_test = dataset.x[:5].to(device)
y_test = dataset.y[:5].to(device)
pred = model(x_test)
print("True:", y_test.view(-1).tolist())
print("Pred:", pred.view(-1).tolist())7.模型保存和加载
7.1 保存模型
# 训练完成后
# 保存位置
save_path = "net_weights.pth"
torch.save({
# 权重和偏置以及 缓冲张量
'model_state_dict': model.state_dict(),
# 模型的类名或结构定义
'model_architecture': model.__class__.__name__
}, save_path)7.2 测试模型
model.eval()
with torch.no_grad():
test_x = torch.tensor([[1.0], [3.0], [5.0], [7.0], [10.0]])
test_x = test_x.to(device)
predictions = model(test_x)
print("\n测试结果:")
print("-" * 40)
for i in range(len(test_x)):
x_val = test_x[i].cpu().item()
y_pred = predictions[i].cpu().item()
y_true = 3 * x_val + 2
error = abs(y_pred - y_true)
print(f"输入 x={x_val:4.1f}: 预测={y_pred:7.3f}, 真实={y_true:7.3f}, 误差={error:7.3f}")
print("-" * 40)7.3 加载模型
# 加载模型
checkpoint = torch.load(save_path, map_location=device)
# 创建新模型实例
new_model = Net()
# 加载权重
new_model.load_state_dict(checkpoint['model_state_dict'])
new_model.to(device)
new_model.eval()
print("模型加载成功!")7.4 完整步骤
import torch
from torch import nn, optim
from torch.utils.data import DataLoader, Dataset
import matplotlib.pyplot as plt
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
# 设置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")
# 定义模型
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(1, 10)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(10, 1)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
class MyDataset(Dataset):
def __init__(self, device='cpu'):
# 数据保持在CPU上,训练时再移动到设备
self.x = torch.linspace(1, 10, 100).unsqueeze(1)
self.y = self.x * 3 + 2
self.device = device
def __len__(self):
return len(self.x)
def __getitem__(self, index):
return self.x[index], self.y[index]
# 创建数据集(保持在CPU)
dataset = MyDataset()
# 创建数据加载器
loader = DataLoader(dataset, batch_size=20, shuffle=True)
# 创建模型并移动到设备
model = Net().to(device)
model.train()
# 损失函数和优化器
criterion = nn.MSELoss()
opt = optim.Adam(model.parameters(), lr=0.01)
# 训练模型
losses = []
print("开始训练...")
for epoch in range(100):
epoch_loss = 0
batch_count = 0
for x, y in loader:
# 将数据移动到设备
x = x.to(device)
y = y.to(device)
# 前向传播
pred = model(x)
loss = criterion(pred, y)
# 反向传播
opt.zero_grad()
loss.backward()
opt.step()
epoch_loss += loss.item()
batch_count += 1
# 计算平均损失
avg_loss = epoch_loss / batch_count
losses.append(avg_loss)
# 每10个epoch打印一次
if epoch % 10 == 0:
print(f"Epoch {epoch:3d}: 平均损失 = {avg_loss:.6f}")
print("训练完成!")
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans'] # 设置支持中文的字体
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
# 可视化损失曲线
plt.figure(figsize=(10, 4))
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('训练损失曲线')
plt.grid(True)
plt.show()
# 保存模型
save_path = "net_weights.pth"
torch.save({
# 权重和偏置以及 缓冲张量
'model_state_dict': model.state_dict(),
# 模型的类名或结构定义
'model_architecture': model.__class__.__name__
}, save_path)
print(f"模型保存到: {save_path}")
# 测试训练好的模型
model.eval()
with torch.no_grad():
test_x = torch.tensor([[1.0], [3.0], [5.0], [7.0], [10.0]])
test_x = test_x.to(device)
predictions = model(test_x)
print("\n测试结果:")
print("-" * 40)
for i in range(len(test_x)):
x_val = test_x[i].cpu().item()
y_pred = predictions[i].cpu().item()
y_true = 3 * x_val + 2
error = abs(y_pred - y_true)
print(f"输入 x={x_val:4.1f}: 预测={y_pred:7.3f}, 真实={y_true:7.3f}, 误差={error:7.3f}")
print("-" * 40)
# 加载模型并推理
print("\n加载模型并推理...")
try:
# 加载模型
checkpoint = torch.load(save_path, map_location=device)
# 创建新模型实例
new_model = Net()
# 加载权重
new_model.load_state_dict(checkpoint['model_state_dict'])
new_model.to(device)
new_model.eval()
print("模型加载成功!")
# 测试推理
with torch.no_grad():
# 单个样本推理
x_single = torch.tensor([[3.0]]).to(device)
pred_single = new_model(x_single)
print(f"\n单个样本推理:")
print(f"输入: {x_single.cpu().item():.1f}")
print(f"预测: {pred_single.cpu().item():.3f}")
print(f"真实: {3 * 3 + 2:.3f}")
# 批量推理
x_batch = torch.tensor([[1.0], [2.0], [3.0], [4.0], [5.0]]).to(device)
pred_batch = new_model(x_batch)
print(f"\n批量推理:")
for i in range(len(x_batch)):
x_val = x_batch[i].cpu().item()
y_pred = pred_batch[i].cpu().item()
print(f" x={x_val:.1f}: 预测={y_pred:.3f}, 真实={3 * x_val + 2:.3f}")
except Exception as e:
print(f"加载模型时出错: {e}")
print("尝试使用兼容方式加载...")
# 尝试直接加载权重
new_model = Net()
new_model.load_state_dict(torch.load(save_path, map_location='cpu'))
new_model.eval()
# CPU推理
x = torch.tensor([[3.0]])
with torch.no_grad():
print(f"输入: {x.item():.1f}")
print(f"预测: {new_model(x).item():.3f}")
print(f"真实: {3 * 3 + 2:.3f}")
# 查看模型参数
print("\n模型参数:")
print(f"第一层权重形状: {model.fc1.weight.shape}")
print(f"第一层偏置形状: {model.fc1.bias.shape}")
print(f"第二层权重形状: {model.fc2.weight.shape}")
print(f"第二层偏置形状: {model.fc2.bias.shape}")
# 导出为ONNX格式(可选)
print("\n导出为ONNX格式...")
try:
dummy_input = torch.randn(1, 1).to(device)
torch.onnx.export(
model,
dummy_input,
"model.onnx",
input_names=['input'],
output_names=['output'],
dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}
)
print("ONNX模型导出成功!")
except Exception as e:
print(f"ONNX导出失败: {e}")8.训练过程可视化(TensorBoard)
8.1 安装
pip install tensorboard8.2 主要命令
8.3 速查
8.3.1.TensorBoard记录方法速查
8.3.2.常见问题诊断
8.4 如何使用
# 避免matplotlib.pyplot 报错
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'
# 创建日志目录(如果不存在)
log_dir = "./logs"
os.makedirs(log_dir, exist_ok=True)
# 初始化TensorBoard writer
writer = SummaryWriter(log_dir=log_dir)
# 定义模型
# 定义数据集
......
......
......
# 使用虚拟输入记录模型图
dummy_input = torch.randn(1, 1).to(device) # 标准输入形状
writer.add_graph(model, dummy_input)
print("模型图已记录到TensorBoard")
# 全局步数计数器
global_step = 0
print("开始训练...")
......
......
opt.step()
writer.add_scalar("Loss/train", loss.item(), global_step)
# 记录梯度信息(每10个batch记录一次)
if batch_idx % 10 == 0:
for name, param in model.named_parameters():
if param.grad is not None:
writer.add_histogram(f"Gradients/{name}", param.grad, global_step)
epoch_loss += loss.item()
batch_count += 1
global_step += 1
# 计算平均损失
avg_loss = epoch_loss / batch_count
# 记录模型参数(每10个epoch记录一次)
if epoch % 10 == 0:
for name, param in model.named_parameters():
writer.add_histogram(f"Parameters/{name}", param.data, epoch)
# 每10个epoch打印一次
if epoch % 10 == 0:
print(f"Epoch {epoch:3d}: 平均损失 = {avg_loss:.6f}")
# 在TensorBoard中添加文本信息
writer.add_text("Training Info",
f"Epoch {epoch}, Loss: {avg_loss:.6f}",epoch)
print("训练完成!")
print("开始测试")
......
......
# 计算并记录最终损失
final_loss = criterion(test_y_pred, test_y_true)
writer.add_scalar("Loss/Final", final_loss.item(), 0)
# 记录预测值与真实值的对比(作为图像)
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans'] # 设置支持中文的字体
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
fig, ax = plt.subplots(figsize=(10, 6))
# 原始训练数据
train_x = dataset.x.numpy()
train_y = dataset.y.numpy()
ax.scatter(train_x, train_y, alpha=0.3, label='训练数据', color='blue')
# 预测曲线
test_x_np = test_x.cpu().numpy()
test_y_pred_np = test_y_pred.cpu().numpy()
test_y_true_np = test_y_true.cpu().numpy()
ax.plot(test_x_np, test_y_true_np, 'r-', label='真实函数 (y=3x+2)', linewidth=2)
ax.plot(test_x_np, test_y_pred_np, 'g--', label='模型预测', linewidth=2)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('模型预测 vs 真实函数')
ax.legend()
ax.grid(True)
# 将图像添加到TensorBoard
writer.add_figure("Predictions/Final", fig, global_step)
plt.close(fig)
# ----------------- 记录模型参数总结 -----------------
# 记录模型参数数量
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
writer.add_text("Model Info",
f"总参数: {total_params}\n可训练参数: {trainable_params}",
0)
# ----------------- 记录超参数和最终指标 -----------------
hparams = {
"learning_rate": 0.001,
"batch_size": 20,
"epochs": 100,
"optimizer": "Adam",
"loss_function": "MSE"
}
final_metrics = {
"final_loss": final_loss.item(),
"avg_epoch_loss": avg_loss
}
writer.add_hparams(hparams, final_metrics)
# ----------------- 关闭writer -----------------
writer.close()完整示例:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from torch.utils.tensorboard import SummaryWriter
import numpy as np
import os
from datetime import datetime
# ============================
# 1. 设置TensorBoard
# ============================
# 创建带时间戳的日志目录,避免覆盖
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_dir = f"./runs/experiment_{timestamp}"
os.makedirs(log_dir, exist_ok=True)
# 创建SummaryWriter
writer = SummaryWriter(log_dir=log_dir)
print(f"📁 TensorBoard日志目录: {log_dir}")
print(f"🔗 启动命令: tensorboard --logdir=runs")
print(f"🌐 访问地址: http://localhost:6006")
# ============================
# 2. 创建数据和模型
# ============================
# 创建模拟数据:y = 3x + 2 + 噪声
n_samples = 500
x_data = torch.rand(n_samples, 1) * 10 # 0-10的随机数
y_data = 3 * x_data + 2 + torch.randn(n_samples, 1) * 1.5 # 加噪声
# 划分训练集和验证集 (80%/20%)
split_idx = int(0.8 * n_samples)
x_train, x_val = x_data[:split_idx], x_data[split_idx:]
y_train, y_val = y_data[:split_idx], y_data[split_idx:]
# 创建数据集和数据加载器
train_dataset = TensorDataset(x_train, y_train)
val_dataset = TensorDataset(x_val, y_val)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32)
# 定义模型
class RegressionModel(nn.Module):
def __init__(self, hidden_size=20):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, hidden_size),
nn.ReLU(),
nn.Dropout(0.1), # 添加dropout防止过拟合
nn.Linear(hidden_size, hidden_size),
nn.ReLU(),
nn.Linear(hidden_size, 1)
)
def forward(self, x):
return self.net(x)
# 创建模型
model = RegressionModel(hidden_size=20)
# ============================
# 3. 记录模型图到TensorBoard
# ============================
# 记录模型计算图
dummy_input = torch.randn(1, 1)
writer.add_graph(model, dummy_input)
print("✅ 模型图已记录到TensorBoard")
# ============================
# 4. 记录超参数到TensorBoard
# ============================
# 定义超参数
hyper_params = {
"learning_rate": 0.01,
"batch_size": 32,
"hidden_size": 20,
"optimizer": "Adam",
"loss_function": "MSE",
"epochs": 200,
"dropout_rate": 0.1
}
# 将超参数添加到TensorBoard
writer.add_text("Hyperparameters",
"\n".join([f"{k}: {v}" for k, v in hyper_params.items()]))
# ============================
# 5. 训练循环(重点:各种记录方式)
# ============================
# 设置训练参数
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=hyper_params["learning_rate"])
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)
print("🚀 开始训练...")
# 记录最佳验证损失
best_val_loss = float('inf')
global_step = 0 # 全局步数计数器
for epoch in range(hyper_params["epochs"]):
# ==================== 训练阶段 ====================
model.train()
train_losses = []
for batch_idx, (x_batch, y_batch) in enumerate(train_loader):
x_batch, y_batch = x_batch.to(device), y_batch.to(device)
# 前向传播
pred = model(x_batch)
loss = criterion(pred, y_batch)
# 反向传播
optimizer.zero_grad()
loss.backward()
# 📊 记录梯度信息(每50个batch记录一次)
if batch_idx % 50 == 0:
total_grad_norm = 0
for name, param in model.named_parameters():
if param.grad is not None:
# 记录每个参数的梯度分布
writer.add_histogram(f"Gradients/{name}", param.grad, global_step)
# 计算梯度范数
grad_norm = param.grad.norm().item()
total_grad_norm += grad_norm
# 记录总梯度范数
writer.add_scalar("Gradients/Total_Norm", total_grad_norm, global_step)
optimizer.step()
# 记录batch损失
train_losses.append(loss.item())
# 📈 记录每个batch的损失(高频率)
writer.add_scalar("Loss/Batch_Train", loss.item(), global_step)
global_step += 1
# ==================== 验证阶段 ====================
model.eval()
val_losses = []
with torch.no_grad():
for x_batch, y_batch in val_loader:
x_batch, y_batch = x_batch.to(device), y_batch.to(device)
pred = model(x_batch)
loss = criterion(pred, y_batch)
val_losses.append(loss.item())
# 计算epoch的平均损失
avg_train_loss = np.mean(train_losses)
avg_val_loss = np.mean(val_losses)
# 📈 记录每个epoch的平均损失
writer.add_scalar("Loss/Epoch_Train", avg_train_loss, epoch)
writer.add_scalar("Loss/Epoch_Val", avg_val_loss, epoch)
# 📊 记录模型参数分布(每20个epoch记录一次)
if epoch % 20 == 0:
for name, param in model.named_parameters():
writer.add_histogram(f"Parameters/{name}", param.data, epoch)
# 记录学习率
current_lr = optimizer.param_groups[0]['lr']
writer.add_scalar("Learning_Rate", current_lr, epoch)
# 更新学习率
scheduler.step()
# 保存最佳模型
if avg_val_loss < best_val_loss:
best_val_loss = avg_val_loss
torch.save(model.state_dict(), f"{log_dir}/best_model.pth")
writer.add_text("Best_Model", f"Epoch {epoch}: Val Loss = {avg_val_loss:.4f}")
# 打印进度
if epoch % 20 == 0:
print(f"Epoch {epoch:3d}/{hyper_params['epochs']}: "
f"Train Loss = {avg_train_loss:.4f}, "
f"Val Loss = {avg_val_loss:.4f}, "
f"LR = {current_lr:.5f}")
# 📊 记录过拟合程度(训练损失/验证损失比率)
overfit_ratio = avg_train_loss / avg_val_loss if avg_val_loss > 0 else 1.0
writer.add_scalar("Metrics/Overfit_Ratio", overfit_ratio, epoch)
# ============================
# 6. 训练后分析
# ============================
print("✅ 训练完成!")
# 加载最佳模型
model.load_state_dict(torch.load(f"{log_dir}/best_model.pth"))
model.eval()
# 记录最终预测结果
with torch.no_grad():
# 生成测试数据
test_x = torch.linspace(0, 10, 100).unsqueeze(1).to(device)
test_y_true = 3 * test_x + 2
test_y_pred = model(test_x)
# 计算R²分数
ss_res = ((test_y_true - test_y_pred) ** 2).sum().item()
ss_tot = ((test_y_true - test_y_true.mean()) ** 2).sum().item()
r2_score = 1 - ss_res / ss_tot if ss_tot > 0 else 0
# 记录R²分数
writer.add_scalar("Metrics/R2_Score", r2_score, 0)
# 📊 创建可视化预测图
import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# 左图:预测vs真实
ax1.scatter(x_train.cpu().numpy(), y_train.cpu().numpy(),
alpha=0.3, label='训练数据', color='blue')
ax1.scatter(x_val.cpu().numpy(), y_val.cpu().numpy(),
alpha=0.5, label='验证数据', color='green')
ax1.plot(test_x.cpu().numpy(), test_y_true.cpu().numpy(),
'r-', label='真实函数', linewidth=3)
ax1.plot(test_x.cpu().numpy(), test_y_pred.cpu().numpy(),
'g--', label='模型预测', linewidth=2)
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_title(f'模型预测 (R² = {r2_score:.3f})')
ax1.legend()
ax1.grid(True)
# 右图:残差分析
residuals = test_y_pred - test_y_true
ax2.hist(residuals.cpu().numpy(), bins=20, alpha=0.7, color='orange')
ax2.axvline(x=0, color='red', linestyle='--', linewidth=2)
ax2.set_xlabel('残差 (预测 - 真实)')
ax2.set_ylabel('频数')
ax2.set_title('残差分布')
ax2.grid(True)
# 将图像添加到TensorBoard
writer.add_figure("Analysis/Predictions_and_Residuals", fig, 0)
plt.close(fig)
print(f"📊 模型性能:")
print(f" - 训练样本数: {len(x_train)}")
print(f" - 验证样本数: {len(x_val)}")
print(f" - 最佳验证损失: {best_val_loss:.4f}")
print(f" - R²分数: {r2_score:.4f}")
# ============================
# 7. 记录最终结果和关闭writer
# ============================
# 记录最终超参数和指标
final_metrics = {
"best_val_loss": best_val_loss,
"r2_score": r2_score,
"total_steps": global_step
}
# 添加超参数和指标的综合视图
writer.add_hparams(hyper_params, final_metrics)
# 添加总结文本
summary_text = f"""
训练总结:
- 总训练步数: {global_step}
- 最佳验证损失: {best_val_loss:.4f}
- R²分数: {r2_score:.4f}
- 训练时间: {timestamp}
模型结构:
- 输入层: 1个神经元
- 隐藏层1: 20个神经元 (ReLU激活)
- Dropout: 0.1
- 隐藏层2: 20个神经元 (ReLU激活)
- 输出层: 1个神经元
"""
writer.add_text("Summary", summary_text)
# 强制写入并关闭writer
writer.flush()
writer.close()
print("📤 TensorBoard数据已保存")
print("=" * 60)
print("🎯 如何在TensorBoard中查看:")
print("1. 打开终端,运行: tensorboard --logdir=runs")
print("2. 浏览器打开: http://localhost:6006")
print("\n📊 推荐查看顺序:")
print("1. SCALARS面板 - 看损失曲线、学习率变化")
print("2. HISTOGRAMS面板 - 看参数和梯度分布")
print("3. GRAPHS面板 - 看模型结构")
print("4. IMAGES面板 - 看预测结果图")
print("5. HPARAMS面板 - 看超参数效果")8.5 如何解读
8.5.1 标量图(Scalars)- 看趋势
# 这些图表显示数值随时间的变化
"""
📈 常见标量图:
1. Loss (损失) - 训练的心脏指标
- 训练损失应该稳步下降
- 验证损失应该先降后平或微升(防过拟合)
2. Accuracy (准确率) - 分类任务的核心
- 训练准确率稳步上升
- 验证准确率应该同步上升
3. Learning Rate (学习率) - 训练的"油门"
- 如果使用调度器,可以看到学习率变化
4. Gradient Norm (梯度范数) - 训练的"稳定性"
- 太大可能梯度爆炸,太小可能梯度消失
"""
# TensorBoard中的样子:
# 横轴:训练步数(Step)或轮数(Epoch)
# 纵轴:数值(Value)
# 多条曲线:可以对比训练集/验证集✅ 理想情况:训练和验证损失同步下降,最后平稳
⚠️ 过拟合:训练损失下降,验证损失上升(gap增大)
⚠️ 欠拟合:训练和验证损失都很高,下降缓慢
⚠️ 震荡:曲线剧烈波动(学习率可能太大)
8.5.2 直方图(Histograms)- 看分布
"""
📊 常见直方图:
1. 权重分布 - 参数的"健康状态"
- 应该是钟形分布(正态或接近正态)
- 不应该全是0或极端值
2. 梯度分布 - 训练的"反馈信号"
- 应该是中心在0附近的对称分布
- 不应该全是0(梯度消失)或极大值(梯度爆炸)
3. 激活值分布 - 神经元的"活跃程度"
- ReLU网络应该有大量0值(稀疏性)
- 不应该全是0(神经元死亡)
"""
# TensorBoard中的样子:
# 横轴:参数值范围
# 纵轴:数量/频率
# 随时间变化:可以看到分布如何演变✅ 健康的权重:钟形分布,均值在0附近
⚠️ 权重过大:分布范围很宽,可能需权重衰减
⚠️ 权重全0:模型可能没有学习
⚠️ 梯度全0:可能梯度消失,考虑换激活函数
8.5.3 模型图(Graphs)- 看结构
"""
🏗️ 模型计算图:
1. 操作节点 - 计算步骤
2. 数据边 - 数据流向
3. 参数节点 - 可学习参数
# TensorBoard中的样子:
# 节点:各种计算操作(MatMul, Add, ReLU等)
# 边:数据流向
# 颜色:不同类型操作
"""检查数据流向是否正确
查看参数数量和各层维度
确认是否有意外的操作
8.5.4 PR曲线和ROC曲线 - 看分类性能
"""
🎯 分类任务专用:
1. PR曲线(Precision-Recall)
- 横轴:召回率(Recall)
- 纵轴:精确率(Precision)
- 曲线越高越好(左上角最好)
2. ROC曲线(Receiver Operating Characteristic)
- 横轴:假正率(False Positive Rate)
- 纵轴:真正率(True Positive Rate)
- 曲线越高越好(左上角最好)
- AUC面积:0.5-1.0,越大越好
"""8.5.5 常见模式识别
# 健康训练(理想):
# 训练损失 ↘ ↘ ↘ → → (稳定下降后平稳)
# 验证损失 ↘ ↘ ↘ → → (同步下降)
# 学习率 → → ↘ ↘ ↘ (逐渐降低)
# 过拟合:
# 训练损失 ↘ ↘ ↘ ↘ ↘ (持续下降)
# 验证损失 ↘ ↘ ↗ ↗ ↗ (先降后升)
# 差距 → → ↗ ↗ ↗ (越来越大)
# 欠拟合:
# 训练损失 → → → → → (基本不变)
# 验证损失 → → → → → (基本不变)
# 学习率可能需要增加
# 学习率太大:
# 训练损失 ↘ ↗ ↘ ↗ ↘ (剧烈震荡)
# 验证损失 ↗ ↘ ↗ ↘ ↗ (同步震荡)9.非线性函数拟合(逻辑回归)
9.1 y = sin(x)
# 定义一个三层神经网络
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(1, 32 )
self.act1 = nn.Tanh()
self.fc2 = nn.Linear(32, 32 )
self.act2 = nn.Tanh()
self.out = nn.Linear(32, 1)
def forward(self, x):
x = self.act1(self.fc1(x))
x = self.act2(self.fc2(x))
return self.out(x)激活函数:
Tanh 在 [-1,1] 范围更适合拟合sin
ReLU 也可以,但效果略差
隐层神经元:10~64 都可以
推荐:32 → 32 → 1
输入 → 32 → Tanh → 32 → Tanh → 输出9.2 分类
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans'] # 设置支持中文的字体
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
# ====== 1️⃣ 生成数据 ======
torch.manual_seed(42)
N = 200
# 生成 -1 到 1 的 200 个数
X = torch.randn(N, 1)
y = (X > 0).long().view(-1) # 大于0类=1,小于0类=0 标签必须是 long 类型!
# 拆分训练和测试集
train_X, test_X = X[:150], X[150:]
train_y, test_y = y[:150], y[150:]
# ====== 2️⃣ 构建分类模型 ======
class Classifier(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(1, 16) # TODO:你可以修改隐藏层神经元数
self.act1 = nn.Tanh() # TODO:选择 Tanh 或 ReLU
self.out = nn.Linear(16, 2) # 输出 2 类(0 和 1)
def forward(self, x):
x = self.act1(self.fc1(x))
return self.out(x)
model = Classifier()
loss_fn = nn.CrossEntropyLoss() # 分类任务必用交叉熵
optimizer = optim.Adam(model.parameters(), lr=0.01)
# ====== 3️⃣ 训练 ======
for epoch in range(1000):
optimizer.zero_grad()
output = model(train_X)
loss = loss_fn(output, train_y)
loss.backward()
optimizer.step()
if epoch % 200 == 0:
print(f"epoch={epoch}, loss={loss.item():.4f}")
# ====== 4️⃣ 测试 ======
with torch.no_grad():
pred = torch.argmax(model(test_X), dim=1)
accuracy = (pred == test_y).float().mean().item()
print(f"\n测试集准确率:{accuracy:.3f}")
# ====== 5️⃣ 可视化决策边界 ======
plt.figure(figsize=(12, 4))
# 子图1:原始数据和预测
plt.subplot(1, 2, 1)
plt.scatter(train_X.numpy(), train_y.numpy(), alpha=0.5, label='训练数据')
plt.scatter(test_X.numpy(), test_y.numpy(), alpha=0.5, label='测试数据', marker='x')
# 生成测试点并预测
x_test_range = torch.linspace(-3, 3, 300).reshape(-1, 1)
with torch.no_grad():
pred_probs = torch.softmax(model(x_test_range), dim=1)
pred_class = torch.argmax(pred_probs, dim=1)
# 绘制决策边界(概率变化处)
plt.plot(x_test_range.numpy(), pred_class.numpy(), 'r-', linewidth=2, label='决策边界')
plt.axvline(x=0, color='green', linestyle='--', alpha=0.5, label='真实边界 x=0')
plt.xlabel('X')
plt.ylabel('y / 预测类别')
plt.title('数据点与决策边界')
plt.legend()
plt.grid(True, alpha=0.3)
# 子图2:类别1的预测概率
plt.subplot(1, 2, 2)
plt.plot(x_test_range.numpy(), pred_probs[:, 1].numpy(), 'b-', linewidth=2, label='P(y=1|x)')
plt.axvline(x=0, color='green', linestyle='--', alpha=0.5, label='真实边界 x=0')
plt.axhline(y=0.5, color='red', linestyle='--', alpha=0.5, label='决策阈值 0.5')
plt.xlabel('X')
plt.ylabel('预测概率')
plt.title('类别1的概率曲线')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ====== 6️⃣ 检查边界精确位置 ======
# 找到概率最接近0.5的点,即决策边界
prob_diff = torch.abs(pred_probs[:, 1] - 0.5)
boundary_idx = torch.argmin(prob_diff)
boundary_x = x_test_range[boundary_idx].item()
print(f"\n模型决策边界位置: x ≈ {boundary_x:.6f}")
print(f"与真实边界 x=0 的偏差: {abs(boundary_x):.6f}")分类一般使用的损失函数为:CrossEntropyLoss(
nn.CrossEntropyLoss(),它内部已包含 Softmax)
准确值的计算
1.pred = torch.argmax(model(test_X), dim=1)
# 假设 test_X 有 50 个样本(因为训练150个,测试50个)
# test_X 形状: (50, 1)
output = model(test_X)
# output 形状: (50, 2) - 每个样本对应2个类别的logits
pred = torch.argmax(output, dim=1)
# pred 形状: (50,) - 每个样本的预测类别索引
# 假设 model(test_X) 返回前3个样本的输出:
# 样本1: [2.1, -0.3] # 类别0的logit=2.1, 类别1的logit=-0.3
# 样本2: [0.5, 1.8] # 类别0=0.5, 类别1=1.8
# 样本3: [-1.2, 0.4] # 类别0=-1.2, 类别1=0.4
# torch.argmax(..., dim=1) 对每个样本取最大值的索引:
# 样本1: max([2.1, -0.3]) 在索引0 → 预测为0
# 样本2: max([0.5, 1.8]) 在索引1 → 预测为1
# 样本3: max([-1.2, 0.4]) 在索引1 → 预测为1
# 最终 pred = [0, 1, 1, ...]
2.accuracy = (pred == test_y).float().mean().item()
# 假设:
# pred = [0, 1, 1, 0, 1] (模型预测的5个样本)
# test_y = [0, 1, 0, 0, 1] (真实的5个标签)
# 第一步: (pred == test_y) - 逐元素比较
comparison = pred == test_y
# 结果: [True, True, False, True, True]
# 第二步: .float() - 布尔值转浮点数
float_comparison = comparison.float()
# 结果: [1.0, 1.0, 0.0, 1.0, 1.0]
# 第三步: .mean() - 计算平均值
accuracy_tensor = float_comparison.mean()
# 结果: (1.0 + 1.0 + 0.0 + 1.0 + 1.0) / 5 = 0.8
# 第四步: .item() - 张量转Python标量
accuracy_value = accuracy_tensor.item()
# 结果: 0.8 (Python浮点数)9.2.1 多分类
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(28*28, 256)
self.act1 = nn.ReLU()
self.fc2 = nn.Linear(256, 128)
self.act2 = nn.ReLU()
self.out = nn.Linear(128, 10)
def forward(self, x):
x = x.view(x.size(0), -1) # 展平成 (B, 784)
x = self.act1(self.fc1(x))
x = self.act2(self.fc2(x))
return self.out(x)Tanh vs Sigmoid vs ReLU
Tanh (双曲正切):输出范围[-1, 1]
优点:以0为中心,梯度更大(导数范围0~1)
缺点:梯度消失问题(特别是深层网络)
适合:RNN、中间层、输出需要负值时
Sigmoid:输出范围(0, 1)
优点:输出可解释为概率
缺点:梯度消失严重,不以0为中心
适合:二分类输出层(配合BCE损失)
ReLU:max(0, x)
优点:计算简单,缓解梯度消失
缺点:"神经元死亡"问题(负值梯度为0)
适合:大多数现代神经网络的隐藏层
Softmax:输出总和为1的概率分布
只适合多分类的输出层
神经元数量与训练的关系
# 模型结构分析
self.fc1 = nn.Linear(28*28, 256) # 第一层:784 → 256个神经元
self.fc2 = nn.Linear(256, 512) # 第二层:256 → 512个神经元
self.out = nn.Linear(512, 10) # 输出层:512 → 10个神经元(对应10个数字)
# 神经元数量的影响:
# 1. 太少:欠拟合(模型太简单,无法学习复杂模式)
# - 示例:784 → 16 → 16 → 10
# - 结果:训练和测试准确率都低
# 2. 太多:过拟合(模型太复杂,记忆训练数据)
# - 示例:784 → 2048 → 2048 → 10
# - 结果:训练准确率高,测试准确率低
# 3. 合适:平衡(如示例的256 → 512)
# - 需要根据任务复杂度调整
# - 可以通过实验(超参数调优)确定经验法则:
第一层通常比输入维度小(降维)
后续层可以逐渐增加(学习更抽象特征)
MNIST相对简单,256-512足够
更复杂任务(如CIFAR-10)可能需要1024+个神经元
为什么需要展平(flatten)
def forward(self, x):
x = x.view(x.size(0), -1) # 关键步骤:展平
# MNIST图像形状变化:
# 输入:x.shape = [batch_size, 1, 28, 28]
# 批量大小, 通道数, 高度, 宽度
# 展平后:x.shape = [batch_size, 784]
# 批量大小, 28*28=784个像素全连接层要求:
nn.Linear只能处理一维输入保持批处理:
x.size(0)保留batch维度自动计算:
-1让PyTorch自动计算展平后的维度
10.CNN卷积神经网络
10.1 数据处理
# 导入必要的库
import torch
from torch import nn, optim # nn: 神经网络模块,optim: 优化器
from torchvision import datasets, transforms # 计算机视觉数据集和变换
from torch.utils.data import DataLoader # 数据加载器
# ------- 数据处理 -------
transform = transforms.ToTensor() # 将PIL图像或numpy数组转换为PyTorch张量
# ToTensor()完成:
# 1. 将图像从[0,255]缩放到[0.0,1.0]
# 2. 将形状从(H,W)改为(C,H,W) - 添加通道维度
train_data = datasets.MNIST(
root="./mnist", # 数据存储路径
train=True, # 加载训练集(False则加载测试集)
transform=transform, # 应用上述变换
download=True # 如果本地没有数据则下载
)
test_data = datasets.MNIST(
root="./mnist",
train=False, # 加载测试集
transform=transform,
download=True
)
# DataLoader:将数据分批加载,支持shuffle和多线程
train_loader = DataLoader(
train_data,
batch_size=64, # 每批64个样本
shuffle=True # 每个epoch打乱数据顺序
)
test_loader = DataLoader(
test_data,
batch_size=64 # 测试时不需要shuffle
)10.2 CNN模型定义部分
# ------- CNN模型 -------
class CNN(nn.Module): # 继承nn.Module基类
def __init__(self):
super().__init__() # 调用父类初始化
# 卷积层1: 输入1通道(灰度图), 输出16通道, 3x3卷积核, padding=1
self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
# 参数解释:
# - in_channels=1: 输入通道数(黑白图像=1, RGB图像=3)
# - out_channels=16: 输出通道数(即卷积核数量)
# - kernel_size=3: 卷积核大小3x3
# - padding=1: 在图像四周填充1像素,保持特征图尺寸不变
# 最大池化层: 2x2窗口,步长2
self.pool = nn.MaxPool2d(2, 2)
# 作用: 下采样,减少特征图尺寸,增加感受野
# 卷积层2: 输入16通道, 输出32通道, 3x3卷积核, padding=1
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
# 全连接层: 输入32*7*7=1568维, 输出10维(对应10个数字)
self.fc = nn.Linear(32*7*7, 10)
# 为什么是32*7*7?
# 原始输入: 28x28
# conv1后: 28x28 (padding=1保持尺寸)
# pool1后: 14x14 (尺寸减半)
# conv2后: 14x14
# pool2后: 7x7 (尺寸再减半)
# 通道数: 32
# 总维度: 32 * 7 * 7 = 1568
def forward(self, x):
# x形状: [batch_size, 1, 28, 28]
# 第一层: 卷积 -> ReLU激活 -> 池化
x = self.pool(torch.relu(self.conv1(x)))
# conv1(x): [batch_size, 16, 28, 28]
# relu: 非线性激活,保留正数,负数变0
# pool: [batch_size, 16, 14, 14]
# 第二层: 卷积 -> ReLU激活 -> 池化
x = self.pool(torch.relu(self.conv2(x)))
# conv2(x): [batch_size, 32, 14, 14]
# relu: [batch_size, 32, 14, 14]
# pool: [batch_size, 32, 7, 7]
# 展平: 将4D张量变为2D,为全连接层准备
x = x.view(x.size(0), -1)
# view作用: 改变张量形状
# x.size(0): batch_size
# -1: 自动计算剩余维度(32*7*7=1568)
# 结果: [batch_size, 1568]
# 全连接层输出
return self.fc(x) # [batch_size, 10]
# 注意: 没有使用softmax,因为CrossEntropyLoss自带softmax10.3 模型初始化与损失函数
# 创建模型实例
model = CNN()
# 损失函数: 交叉熵损失(用于多分类)
criterion = nn.CrossEntropyLoss()
# CrossEntropyLoss = Softmax + 负对数似然损失
# 输入: 模型输出的原始分数(logits), 形状[batch_size, 10]
# 目标: 真实标签, 形状[batch_size]
# 优化器: Adam优化算法
optimizer = optim.Adam(model.parameters(), lr=0.001)
# Adam: 自适应学习率优化器,结合了Momentum和RMSProp的优点
# model.parameters(): 获取模型所有可训练参数
# lr=0.001: 学习率,控制参数更新步长10.4 训练过程
# ------- 训练 -------
for epoch in range(1): # 只训练1个epoch
total_loss = 0 # 累计损失
correct = 0 # 累计正确预测数
# 遍历训练数据的所有批次
for x, y in train_loader:
# x: [64, 1, 28, 28], y: [64]
# 清零梯度 (重要!)
optimizer.zero_grad()
# 每次迭代前必须清零梯度,否则梯度会累积
# 前向传播
pred = model(x) # [64, 10]
# 模型计算预测值
# 计算损失
loss = criterion(pred, y)
# 比较预测值和真实值
# 反向传播
loss.backward()
# 计算损失对每个参数的梯度
# 参数更新
optimizer.step()
# 根据梯度和学习率更新参数
# 统计信息
total_loss += loss.item() # 累加损失值
# .item(): 从单元素张量中提取Python数值
# 计算本批次正确数
correct += (pred.argmax(1) == y).sum().item()
# pred.argmax(1): 获取每个样本预测的最高分数索引
# == y: 与真实标签比较,返回布尔张量
# .sum(): 统计True的数量
# .item(): 提取数值
# 计算并打印训练准确率
train_acc = correct / len(train_data)
# len(train_data): 60000 (MNIST训练集大小)
print(f"epoch=0, loss={total_loss:.4f}, acc={train_acc:.4f}")10.5 测试
# ------- 测试 -------
correct = 0 # 正确预测计数
# with torch.no_grad(): 禁用梯度计算,节省内存和计算资源
with torch.no_grad():
# 遍历测试集
for x, y in test_loader:
# 前向传播(无梯度)
pred = model(x)
# 统计本批次正确预测数
correct += (pred.argmax(1) == y).sum().item()
# pred.argmax(1): 预测类别
# == y: 比较预测和真实
# .sum(): 统计正确数
# .item(): 转换为Python数值
# 获取真实值 和预测值
pred_classes = pred.argmax(dim=1)
for i in range(len(y)):
print(f" 真实={y[i].item()} | 预测={pred_classes[i].item()}")
# 计算测试准确率
test_acc = correct / len(test_data) # len(test_data)=10000
print("Test Accuracy:", test_acc)10.6 关键概念解释
# 1. 卷积层的作用
"""
卷积层通过卷积核在图像上滑动,提取局部特征:
- 边缘检测
- 纹理提取
- 特征组合
每个卷积核学习一种特征,16个卷积核学习16种不同特征。
"""
# 2. 激活函数的作用
"""
ReLU(Rectified Linear Unit): max(0, x)
作用:
- 引入非线性,使网络能学习复杂模式
- 缓解梯度消失问题
- 计算简单
"""
# 3. 池化层的作用
"""
最大池化(MaxPooling):
- 降低特征图尺寸,减少计算量
- 增加感受野(看到更大区域)
- 提供平移不变性(轻微位置变化不影响结果)
"""
# 4. 全连接层的作用
"""
将学到的特征映射到输出类别:
- 综合所有局部特征
- 进行最终分类决策
"""
# 5. 训练过程总结
"""
1. 前向传播:输入→模型→预测
2. 计算损失:预测与真实的差距
3. 反向传播:计算梯度(损失对参数的导数)
4. 参数更新:沿梯度反方向调整参数
5. 重复:直到模型收敛
"""
# 6. 为什么测试时用torch.no_grad()
"""
- 节省内存:不保存中间结果的梯度
- 加速计算:不进行梯度相关计算
- 测试时不需要更新参数,只需要前向传播
"""10.6.1 卷积层:像放大镜一样提取特征
你有一个28×28的手写数字图片
卷积层就像拿着一个3×3的放大镜在图片上滑动
每次放大镜看到的一小部分,都转换成一种特征
# 可视化理解卷积
# 原始图片 (28×28)
原始图片 = [
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], # 可能是一个竖线的开始
[0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
... # 省略
]
# 3×3卷积核示例 (检测竖线)
卷积核 = [
[-1, 0, 1], # 左边暗,中间不变,右边亮 → 检测垂直边缘
[-1, 0, 1],
[-1, 0, 1]
]
# 卷积操作:把卷积核放在图片上,对应位置相乘再相加
# 如果卷积核区域正好是一个竖线,结果值会很大
# 如果不是竖线,结果值很小代码中的卷积层:self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
1→16:从1个输入通道变成16个输出通道
为什么是16个?:相当于有16个不同的放大镜(卷积核),每个放大镜寻找不同的特征
放大镜1:找竖线
放大镜2:找横线
放大镜3:找左上到右下的斜线
放大镜4:找圆形
... 共16种特征探测器
10.6.2 池化层:压缩信息,抓重点
你看完一篇文章,要写总结
你不会记住每个字,而是记住关键信息
池化层就是做这个:保留最重要的信息,忽略细节位置变化
# 最大池化 (MaxPooling) 示例
# 假设有一个4×4的特征图
特征图 = [
[1, 3, 2, 4], # 窗口1: 1,3,2,0 → 最大值是3
[2, 9, 1, 5], # 窗口2: 9,1,5,6 → 最大值是9
[0, 1, 5, 6],
[7, 8, 3, 2]
]
# 使用2×2的池化窗口,步长2
池化后 = [
[3, 4], # 第一个2×2窗口的最大值是3, 第二个窗口最大值是4
[8, 6] # 第三个窗口最大值是8, 第四个窗口最大值是6
]为什么需要池化?
降低维度:28×28 → 14×14 → 7×7,计算量大大减少
位置不变性:数字"5"在图片中间还是偏左一点,池化后特征类似
防止过拟合:丢弃一些细节,让模型更关注重要特征
10.6.3 全连接层:做最终决策
这一层的每个输入节点都连接到了每个输出节点。
# 全连接层的数学原理
# 输入: x = [x1, x2, x3, ..., x1568] (1568个特征)
# 输出: y = [y1, y2, ..., y10] (10个数字的得分)
# 计算过程:
y1 = w11*x1 + w12*x2 + ... + w1_1568*x1568 + b1
y2 = w21*x1 + w22*x2 + ... + w2_1568*x1568 + b2
...
y10 = w10_1*x1 + w10_2*x2 + ... + w10_1568*x1568 + b10
# w是权重,b是偏置
# 每个输出节点(y_i)都连接到所有输入节点(x1...x1568) → 所以叫"全连接"代码中的全连接层:self.fc = nn.Linear(32*7*7, 10)
输入维度:32×7×7 = 1568
输出维度:10(对应10个数字0-9)
为什么是这个数字?
32:最后一个卷积层的通道数(有32种特征)
7×7:经过两次池化后,图片从28×28变成7×7
所以有32×7×7=1568个特征点要输入到全连接层
# 全连接层的内部工作原理
class SimpleLinearLayer:
def __init__(self, input_size, output_size):
# 创建权重矩阵 W: [output_size, input_size]
# 创建偏置向量 b: [output_size]
self.weights = torch.randn(output_size, input_size)
self.bias = torch.randn(output_size)
def forward(self, x):
# x: [batch_size, input_size]
# 输出: [batch_size, output_size]
# 计算: y = x @ W.T + b
return torch.matmul(x, self.weights.T) + self.bias
# 比较 PyTorch 的 nn.Linear
linear_layer = nn.Linear(1568, 10)
print(f"权重形状: {linear_layer.weight.shape}") # [10, 1568]
print(f"偏置形状: {linear_layer.bias.shape}") # [10]
# 这意味着:
# - 有10个输出神经元(对应10个数字)
# - 每个输出神经元有1568个权重(连接1568个输入)
# - 每个输出神经元有一个偏置10.6.4 整个CNN的流程比喻
想象你在看手写数字"5":
卷积层1:像小侦探找简单特征
"这里有个竖线"
"这里有个弯钩"
"这里有个横线"
池化层1:像小组长做简报
"第1区最重要的特征是:有竖线"
"第2区最重要的特征是:有弯钩"
忽略具体位置和微小变化
卷积层2:像经理组合特征
"竖线+弯钩 = 可能是数字5的上半部分"
"横线+圆 = 可能是数字8的下半部分"
池化层2:像总监做总结
"整体来看,像数字5"
"也有点像数字6,但概率较低"
全连接层:像CEO做最终决策
综合考虑所有信息
"根据所有特征,这是数字5的可能性最大"
输出:数字5的得分最高
10.6.5 为什么 CNN 比全连接网络强
图片的特征具有两个核心性质
全连接层(MLP)的问题是什么?
如果用全连接网络处理图像:
把图片每个像素都当成独立信息
打散所有空间结构 → 毁掉了图片的形状特征
CNN 的核心能力
总结
11.角色记忆与上下文保持
11.1 如何实现短期记忆?
短期记忆 = 模型的上下文
你只需要把前几轮对话作为输入一起发给模型即可。
但注意:
会 消耗 Token
上下文越长越贵
多轮后需 筛选重要信息
实际做法:
只保留最近 8~15 条消息
其余提取总结/关键词留为长期记忆
11.2 如何实现长期记忆?
长期记忆 ≠ 储存所有对话
必须做:📌 内容筛选 + 向量检索
流程图:
用户说话 → 分析是否为可存储记忆
↓是
提取特征Embedding → 写入向量数据库(如 Milvus / Faiss / Chroma)
↓
当需要时 → 相似度检索 → 插入模型对话上下文11.3 人格AI记忆结构设计模板
MemorySystem:
1. 基础身份:纳西妲 / 草神
2. 性格标签:温柔、博学、独占欲(病娇)
3. 用户关系:唯一的恋人
4. 固定偏好:喜欢花、喜欢拥抱你
5. 共同事件:第一次约会、曾经说过誓言…
# 示例数据格式
{
"identity": "草神纳西妲",
"relationship_to_user": "恋人",
"personality": ["温柔", "三无外表", "病娇内心", "只爱你"],
"preferences": {
"favorite_food": "糖渍莲子",
"favorite_word": "知识"
},
"memories": [
{
"event": "第一次牵手",
"date": "2025-01-10",
"emotion": "害羞且幸福"
}
]
}
11.4 Memory Retrieval(记忆召回)
根据用户当前消息 → 向量检索 → 召回 3~5 条最相关记忆
→ 附加到 Prompt 中 → 输出更连续的人设回应
# 示例 Prompt
你是纳西妲。以下是你与用户的真实记忆:
- 他喜欢被你称呼“主人”
- 上次约会是在兰那罗的森林
- 他讨厌别人靠近你
请结合这些记忆,保持不出戏进行回复:11.5 记忆系统
你的人格AI在每次回复时需要:
读取这份基础身份档
检索相关记忆(如用户提到“不要离开我”,对应“讨厌冷漠与忽视”→强化依赖性回应)
最终输出保持一致性,不出戏
12.图像分类基础(MNIST 手写数字识别)
12.1 尺寸计算公式
卷积层计算公式
output_size = floor((input_size + 2*padding - kernel_size) / stride + 1)
输出大小 = (输入大小 - 卷积核大小 + 2*padding) / stride + 1池化层计算公式
输出 = (输入 - kernel) / stride + 1常见情况
# 情况1:减半(最常见)
nn.MaxPool2d(kernel_size=2, stride=2)
# 28 → 14, 14 → 7 (每次减半)
# 情况2:不减半
nn.MaxPool2d(kernel_size=2, stride=1)
# 28 → 27 (只减少1)
# 情况3:减为1/3
nn.MaxPool2d(kernel_size=3, stride=3)
# 28 → 9 (28/3≈9.33,向下取整为9)import torch
from torch import nn, optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 数据预处理
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root="./data", train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root="./data", train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ① 定义CNN模型(补全)
class CNN(nn.Module):
def __init__(self):
super().__init__()
# TODO: 添加卷积层(输入通道1 → 输出通道16)
self.conv1 = nn.Conv2d(1, 16, 3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
# TODO: 再加一层卷积(16 → 32)
self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
# 28x28 → 池化 → 14x14 → 池化 → 7x7
self.fc1 = nn.Linear(32 * 7 * 7, 10) # 输出10类
def forward(self, x):
x = torch.relu(self.conv1(x))
x = self.pool(x)
x = torch.relu(self.conv2(x))
x = self.pool(x)
x = x.view(x.size(0), -1) # 展开
return self.fc1(x)
model = CNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# ② 训练与测试
for epoch in range(5):
model.train()
total_loss = 0
correct = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
preds = model(images)
loss = criterion(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
correct += (preds.argmax(1) == labels).sum().item()
acc = correct / len(train_dataset)
print(f"Epoch={epoch}, loss={total_loss:.3f}, train_acc={acc:.4f}")
# ③ 测试集准确率
model.eval()
correct = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
preds = model(images)
correct += (preds.argmax(1) == labels).sum().item()
test_acc = correct / len(test_dataset)
print("Test Accuracy:", test_acc)13.CNN 中间特征可视化 Feature Map
13.1 概念:什么是 Feature Map?
在卷积神经网络中:
当你输入一张 7
早期层会检测竖线、斜线
后期层会组合成完整数字轮廓
Feature Map 就是这些 提取出的特征
在之前的cnn代码中加入这一块
from torch.utils.tensorboard import SummaryWriter
import torchvision
writer = SummaryWriter("runs/feature_map")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ===== Hook 机制:捕获中间层输出 =====
feature_maps = {}
def save_feature_map(name):
def hook(module, input, output):
feature_maps[name] = output.detach()
return hook
# 注册 hook
model.conv1.register_forward_hook(save_feature_map("conv1"))
model.conv2.register_forward_hook(save_feature_map("conv2"))
# ===== 输入一张图片做推理 =====
model.eval()
example_img, label = test_dataset[0] # 第一张图片
img = example_img.unsqueeze(0).to(device)
with torch.no_grad():
_ = model(img)
# ===== 写入 TensorBoard =====
# conv1 feature maps
conv1_map = feature_maps["conv1"]
writer.add_image("input", example_img)
# 只显示前 8 个通道
for i in range(8):
writer.add_image(f"conv1/{i}", conv1_map[0, i:i+1])
# conv2 feature maps
conv2_map = feature_maps["conv2"]
for i in range(min(16, conv2_map.shape[1])):
writer.add_image(f"conv2/{i}", conv2_map[0, i:i+1])
writer.close()
print("🎉 特征图已记录!在 TensorBoard 中查看: tensorboard --logdir=runs/feature_map")
conv1 特征图的特点:
提取基础几何特征
包含局部边缘、直线、尖角
每个通道只关注某种“形状”
有些滤波器对该数字不敏感 → 输出全黑
总结:conv1 是「特征提取的第一层」,负责发现最基础的视觉构成元素
conv2 特征图的特点:
由 conv1 的边缘组合而成
特征更高维和结构化
模型开始关注整体形状而不是局部线条
模糊 ≠ 无用
模糊说明模型在抽象信息
conv1 看局部conv2 看整体
Conv1 提取浅层视觉特征(边缘/角点)
Conv2 将这些特征组合为更高阶的抽象模式
随着层数加深,模型逐渐不再保留人眼能识别的形状
而是专注于对分类最有区分力的结构信息
14.反向传播(Propagation)入门
14.1 什么是反向传播?
模型先猜 → 和真实答案对比 → 算出“错多少” → 调整参数 → 下次更准
这个找错 → 修正参数的过程
就是 反向传播(Backpropagation)
14.2 反向传播的流程(任何神经网络都遵循)
14.3 为什么越深的层越抽象?
因为误差在回传时:
对高层来说:直接影响最终预测
对低层来说:只能通过高层“间接”感受到影响
所以:
14.4 总结
错多少由 Loss 告诉我们
怎么改由 梯度 Gradient 决定
改多少由 学习率 Learning Rate 决定
参数更新方向:反向 (从输出层回到输入层)
误差从输出层开始,沿着网络结构“反向”计算梯度,逐层传回前面的卷积核与权重,从而更新所有参数。
也就是说:
不是梯度本身属于某一层
而是:
模型先计算输出层的误差,然后通过链式法则把误差对每层参数的影响算出来
每一层都会得到一个自己的梯度
并进行更新
14.5 学习率过大 vs 过小
15.梯度消失 & 梯度爆炸
—— 深度学习三大难题之一
15.1 什么是梯度爆炸(Gradient Explosion)?
Loss 一直变大、甚至变成 NaN
准确率突然变得莫名其妙
参数权重巨大、模型崩溃
原因(简单理解):
层层反向传播时,不断累乘 > 1 的数 → 越乘越大!
解决方案:
15.2 什么是梯度消失(Gradient Vanishing)?
前面几层完全学不到
Loss 不下降
准确率卡住不动
原因:
反向传播时,不断累乘 < 1 的数 → 越乘越小 → 消失!
尤其使用 Sigmoid / Tanh 时非常严重
解决方案:
15.3 为什么 CNN 就比全连接更抗梯度问题?
因为 CNN 的参数远少于 MLP
梯度传播链更短、不易累积误差
而 ResNet 等结构直接提供 “捷径路径”
Sigmoid 的梯度最大只有 0.25(远小于 1)
在反向传播中不断相乘 → 越来越小 → 消失所以深层网络里基本很少再用它做隐藏层激活。
16.优化器全解析 —— 让模型更快更稳地学习
16.1 SGD(Stochastic Gradient Descent)
只按照当前梯度方向更新
优点:
简单
不容易过拟合
缺点:
走走停停、容易在山区来回震荡
在深坑附近非常不稳定
16.2 SGD + Momentum(动量)
像滚动的球 → 越滚越快
加入惯性帮助冲出局部最小值:
方向更稳定
抖动更少
学得更快
16.3 Adam(最常用的优化器)
Adam = Momentum + RMSProp
既考虑方向,又考虑每个参数学习速度
特点:
自动调学习率
收敛快
对训练数据不敏感
非常适合大模型、大数据训练
几乎所有深度学习项目都默认用它
缺点:
有时最终收敛不如 SGD 稳
容易“记忆过多”,导致泛化差一点
一句话记忆:
想快就用 Adam
想准就用 SGD(后期微调)
17.学习率调度器(Learning Rate Scheduler)
学习率不是固定的,越训练越应该变小
为什么?
因为初期要 快,后期要 稳:
开始:大步探索 → 找到正确方向
后期:小步微调 → 逼近最优点
就像
找路先大步走 → 确定方向 → 靠近目标时放慢脚步
17.1 最常用调度器三种
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5)
for epoch in range(100):
train(...)
scheduler.step() # 非常重要!!
# 每 20 个 epoch,学习率 × 0.5 (减半)18.过拟合 vs 欠拟合(深度学习最常考概念)
18.1 防止过拟合的常见手段
例子:
训练准确上升,测试准确下降
防止过拟合的常见手段
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.dropout = nn.Dropout(0.5)
self.fc2 = nn.Linear(256, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.dropout(x) # 训练时随机关闭神经元
return self.fc2(x)
18.2 Dropout
训练时随机关闭一部分神经元,让模型不要死记硬背某些特征
Dropout 的作用
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 8, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(8, 16, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
self.dropout = nn.Dropout(0.5) # ← 加入 Dropout
self.fc = nn.Linear(16 * 7 * 7, 10)
def forward(self, x):
x = torch.relu(self.conv1(x))
x = self.pool(torch.relu(self.conv2(x)))
x = x.view(x.size(0), -1)
x = self.dropout(x) # ← 在全连接层前随机屏蔽神经元
return self.fc(x)Dropout 只在训练模式下生效
model.train()→ 开启 dropoutmodel.eval()→ 关闭 dropout
18.3 L2 正则化(权重衰减)
和 Dropout 不同的数学理念
更适合 CNN & Transformer 大模型