PyTorch深度学习实战:从模型构建到训练技巧
本文将通过18个实战代码示例,带你深入掌握PyTorch的核心技术,包括数据准备、模型构建、自定义层、参数初始化、损失函数设计等关键知识点。
目录
- [1. 数据准备与可视化](#1. 数据准备与可视化)
- [2. 并行网络架构设计](#2. 并行网络架构设计)
- [3. 模型训练与可视化](#3. 模型训练与可视化)
- [4. 简化网络与参数初始化](#4. 简化网络与参数初始化)
- [5. 自定义Sequential容器](#5. 自定义Sequential容器)
- [6. 自定义参数与固定模块](#6. 自定义参数与固定模块)
- [7. 自定义损失函数](#7. 自定义损失函数)
- [8. 参数访问与模块管理](#8. 参数访问与模块管理)
- [9. 动态添加模块](#9. 动态添加模块)
- [10. 参数手动修改与梯度陷阱](#10. 参数手动修改与梯度陷阱)
- [11. 参数初始化方法](#11. 参数初始化方法)
- [12. 深度自编码器实现](#12. 深度自编码器实现)
- [13. detach与梯度分离](#13. detach与梯度分离)
1. 数据准备与可视化
1.1 多项式特征数据生成
在开始深度学习之前,我们首先准备训练数据。这里模拟一个多项式回归问题,通过构造X的多次幂作为特征,来学习一个非线性关系。
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader, TensorDataset
# ==================== 数据准备 ====================
# 定义训练集和测试集大小
train_num = 150
test_num = 20
high = 1
low = -1
# 生成基础特征X:在[-1, 1]区间内均匀分布
X = torch.rand(size=(train_num + test_num, 1)) * (high - low) + low
# ==================== 多项式特征构造 ====================
# 构造高维特征:[X, X², X³, X⁴, X⁵]
# 注意:切片操作会创建新的内存地址
X_2 = X[:, :] ** 2
X_3 = X[:, :] ** 3
X_4 = X[:, :] ** 4
X_5 = X[:, :] ** 5
X = torch.cat((X, X_2, X_3, X_4, X_5), dim=1)
# ==================== 数据可视化 ====================
# 绘制不同幂次特征的关系图
plt.figure(figsize=(16, 4))
plt.subplot(1, 4, 1)
plt.scatter(X[:, 0], X[:, 3])
plt.title('X vs X³')
plt.subplot(1, 4, 2)
plt.scatter(X[:, 0], X[:, 4])
plt.title('X vs X⁴')
plt.subplot(1, 4, 3)
plt.scatter(X[:, 0], X[:, 1])
plt.title('X vs X²')
plt.subplot(1, 4, 4)
plt.scatter(X[:, 0], X[:, 2])
plt.title('X vs X³')
plt.tight_layout()
plt.show()
print(f"特征矩阵形状: {X.shape}") # torch.Size([170, 5])
1.2 目标值生成
构造线性回归模型,添加噪声使问题更具挑战性。
python
# ==================== 生成目标值 ====================
# 定义真实的权重和偏置
W = torch.tensor([1, 2, 3, 4, 5], dtype=X.dtype)
b = torch.tensor(3, dtype=X.dtype)
# 线性变换:Y = X·W + b
Y = torch.mv(X, W) + b
# 添加高斯噪声,模拟真实数据的不确定性
epsilon = torch.randn(size=Y.shape)
Y += epsilon
# 可视化目标值
fig = plt.figure(figsize=(10, 6))
plt.plot(X[:, 0], Y, 'o', alpha=0.5)
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Polynomial Regression Data with Noise')
plt.show()
1.3 数据加载器创建
使用PyTorch的DataLoader进行批量数据加载和打乱。
python
# ==================== 创建数据集 ====================
# 划分训练集和测试集
train_dataset = TensorDataset(X[:train_num, :], Y[:train_num])
test_dataset = TensorDataset(X[train_num:, :], Y[train_num:])
# 创建数据加载器
train_dataloader = DataLoader(
train_dataset,
batch_size=16, # 批量大小
shuffle=True # 训练时打乱数据
)
test_dataloader = DataLoader(
test_dataset,
batch_size=16,
shuffle=True
)
# ==================== 检查数据加载器 ====================
for X_batch, y_batch in train_dataloader:
print(f"批次形状: X={X_batch.shape}, y={y_batch.shape}")
break
2. 并行网络架构设计
2.1 并行网络原理
并行网络通过多个分支同时处理输入,最后将结果拼接,能够提取更丰富的特征表示。
python
from sklearn import metrics
from tqdm import tqdm
# ==================== 并行网络定义 ====================
class Parallel_Net(nn.Module):
"""
并行网络架构
- 双分支结构:block1和block2同时处理输入
- 输出拼接:将两个分支的特征拼接在一起
"""
def __init__(self, in_channel, out_channel):
super().__init__()
# 分支1:两层全连接网络
self.block1 = nn.Sequential(
nn.Linear(in_channel, out_channel),
nn.ReLU(),
nn.BatchNorm1d(out_channel), # 批归一化:加速收敛
nn.Linear(out_channel, out_channel),
nn.ReLU()
)
# 分支2:三层全连接网络
self.block2 = nn.Sequential(
nn.Linear(in_channel, out_channel),
nn.ReLU(),
nn.BatchNorm1d(out_channel),
nn.Linear(out_channel, out_channel),
nn.ReLU(),
nn.Linear(out_channel, 1) # 输出维度为1
)
def forward(self, X):
# 并行计算两个分支
out1 = self.block1(X)
out2 = self.block2(X)
# 拼接两个分支的输出
return torch.cat((out1, out2), dim=1)
# ==================== 完整网络构建 ====================
# 构建完整网络:输入层 + Dropout + 并行网络 + 输出层
net = nn.Sequential(
nn.Linear(5, 3), # 输入层:5维特征 -> 3维隐藏层
nn.Dropout(0.3), # Dropout:防止过拟合
Parallel_Net(3, 5), # 并行网络:3维 -> 6维(5+1)
nn.Linear(6, 1) # 输出层:6维 -> 1维输出
)
2.2 初始性能评估
在训练前先评估模型的初始性能,作为对比基准。
python
# ==================== 初始性能评估 ====================
# 获取一个批次的数据用于评估
for X_eval, y_eval in train_dataloader:
print(f"评估批次形状: X={X_eval.shape}, y={y_eval.shape}")
break
# 计算初始预测
Y_init = net(X_eval)
initial_loss = metrics.mean_squared_error(
y_eval.detach().numpy(),
Y_init.detach().numpy()
)
print(f"初始MSE损失: {initial_loss:.4f}")
# ==================== 初始预测可视化 ====================
plt.figure(figsize=(12, 6))
plt.plot(range(len(X_eval)), Y_init.detach().numpy(), 'b-', label='Predicted')
plt.plot(range(len(X_eval)), y_eval.numpy(), 'r--', label='True')
plt.xlabel('Sample Index')
plt.ylabel('Value')
plt.title('Initial Model Predictions (Before Training)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
3. 模型训练与可视化
3.1 训练函数定义
封装训练过程,包括前向传播、损失计算、反向传播和参数更新。
python
# ==================== 训练函数定义 ====================
def train_model(train_dataloader, net, lr, epoch):
"""
训练函数
参数:
train_dataloader: 训练数据加载器
net: 待训练的网络模型
lr: 学习率
epoch: 训练轮数
返回:
result_loss: 每轮训练的平均损失列表
"""
# 定义优化器:SGD随机梯度下降
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
# 定义损失函数:MSE均方误差
criterion = nn.MSELoss(reduction="mean")
# 记录每轮的平均损失
result_loss = []
# 开始训练循环
for i in tqdm(range(epoch), desc="Training Progress"):
# 每轮训练
for X_batch, Y_batch in train_dataloader:
# ==================== 前向传播 ====================
optimizer.zero_grad() # 清空梯度
Y_pred = net(X_batch) # 计算预测值
# ==================== 损失计算 ====================
loss_value = criterion(Y_pred, Y_batch.unsqueeze(1))
# ==================== 反向传播 ====================
loss_value.backward() # 计算梯度
optimizer.step() # 更新参数
# ==================== 评估当前轮次性能 ====================
with torch.no_grad(): # 不计算梯度,节省内存
Y_pred = net(X_eval)
mean_loss = metrics.mean_squared_error(Y_pred, Y_batch)
result_loss.append(mean_loss)
# ==================== 绘制训练曲线 ====================
plt.figure(figsize=(10, 6))
plt.plot(range(len(result_loss)), result_loss, 'b-', linewidth=2)
plt.xlabel('Epoch')
plt.ylabel('MSE Loss')
plt.title('Training Loss Curve')
plt.grid(True, alpha=0.3)
plt.show()
return result_loss
# ==================== 开始训练 ====================
result_loss = train_model(
train_dataloader,
net=net,
lr=0.01,
epoch=1000
)
3.2 训练后性能评估
对比训练前后的模型性能。
python
# ==================== 训练后预测 ====================
# 使用训练好的模型进行预测
for X_test, y_test in train_dataloader:
print(f"测试批次形状: X={X_test.shape}, y={y_test.shape}")
break
Y_pred = net(X_test)
# ==================== 预测结果可视化 ====================
plt.figure(figsize=(12, 6))
plt.plot(range(len(X_test)), Y_pred.detach().numpy(), 'b-', label='Predicted')
plt.plot(range(len(X_test)), y_test.numpy(), 'r--', label='True')
plt.xlabel('Sample Index')
plt.ylabel('Value')
plt.title('Model Predictions (After Training)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# 计算最终损失
final_loss = metrics.mean_squared_error(
y_test.numpy(),
Y_pred.detach().numpy()
)
print(f"训练后MSE损失: {final_loss:.4f}")
print(f"损失降低: {((initial_loss - final_loss) / initial_loss * 100):.2f}%")
4. 简化网络与参数初始化
4.1 简化的深度网络
对比并行网络,我们构建一个更简单的深度网络。
python
# ==================== 简化网络定义 ====================
class Net_de(nn.Module):
"""
简化的深度网络
- 两层全连接网络
- 中间使用ReLU激活函数
"""
def __init__(self, in_feature, out_feature):
super().__init__()
self.layer1 = nn.Linear(in_feature, out_feature)
self.layer2 = nn.Linear(out_feature, 1)
def forward(self, X):
X = F.relu(self.layer1(X))
return self.layer2(X)
# 实例化网络
Net_d = Net_de(5, 3)
print("简化网络结构:")
print(Net_d)
4.2 参数初始化方法
参数初始化对训练效果影响巨大,我们对比不同的初始化策略。
python
# ==================== 参数初始化函数 ====================
def init_normal(m):
"""正态分布初始化"""
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=1, std=1)
nn.init.zeros_(m.bias)
def init_constant(m):
"""常数初始化"""
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
def init_xavier(m):
"""Xavier初始化:适合sigmoid/tanh激活函数"""
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
nn.init.zeros_(m.bias)
# ==================== 对比不同初始化方法 ====================
# 方法1:使用自定义网络
Net_d.apply(init_normal)
print("使用正态分布初始化:")
result_loss_1 = train_model(train_dataloader, net=Net_d, lr=0.01, epoch=300)
# 方法2:使用Sequential网络
net_d = nn.Sequential(
nn.Linear(5, 3),
nn.ReLU(),
nn.Linear(3, 1)
)
# 应用Xavier初始化
net_d.apply(init_xavier)
print("\n使用Xavier初始化:")
result_loss_2 = train_model(train_dataloader, net=net_d, lr=0.01, epoch=300)
# 方法3:再次训练自定义网络
Net_d3 = Net_de(5, 3)
Net_d3.apply(init_normal)
print("\n再次训练自定义网络:")
result_loss_3 = train_model(train_dataloader, net=Net_d3, lr=0.01, epoch=300)
4.3 Sequential命名尝试
尝试为Sequential中的模块命名(注意:这种写法在某些PyTorch版本中可能不支持)。
python
# ==================== Sequential命名尝试 ====================
# 注意:以下写法在某些PyTorch版本中可能报错
# PyTorch的Sequential支持两种方式:
# 1. 直接传入模块列表
# 2. 使用OrderedDict(推荐)
from collections import OrderedDict
# 方法1:直接传入(不支持命名)
net_simple = nn.Sequential(
nn.Linear(5, 3),
nn.ReLU(),
nn.Linear(3, 1)
)
# 方法2:使用OrderedDict(支持命名)
net_named = nn.Sequential(
OrderedDict([
('linear1', nn.Linear(5, 3)),
('relu', nn.ReLU()),
('linear2', nn.Linear(3, 1))
])
)
print("简单Sequential结构:")
print(net_simple)
print("\n命名Sequential结构:")
print(net_named)
5. 自定义Sequential容器
5.1 错误的自定义实现
这是一个常见的错误实现,会导致优化器无法获取参数。
python
# ==================== 错误的自定义Sequential ====================
class MySequential_Wrong(nn.Module):
"""
错误的自定义Sequential实现
问题:使用self.modules而非self._modules
结果:优化器无法获取参数,训练时报错
"""
def __init__(self, *args):
super().__init__()
self.modules = {} # 错误:应该使用self._modules
for idx, module in enumerate(args):
self.modules[str(idx)] = module
def forward(self, X):
for m in self.modules.values():
X = m(X)
return X
# 这个实现会导致优化器报错:"optimizer got an empty parameter list"
5.2 正确的自定义实现
正确的自定义Sequential容器,使用_modules来注册参数。
python
# ==================== 正确的自定义Sequential ====================
class MySequential(nn.Module):
"""
正确的自定义Sequential实现
关键:使用self._modules来注册模块
优点:参数会被自动注册,可以被优化器更新
"""
def __init__(self, *args):
super().__init__()
# 关键:使用self._modules(nn.Module的内置属性)
# 这样PyTorch会自动将模块的参数注册到网络中
for idx, module in enumerate(args):
self._modules[str(idx)] = module
def forward(self, X):
# 依次执行每个模块
for m in self._modules.values():
X = m(X)
return X
# ==================== 测试自定义Sequential ====================
# 使用自定义Sequential构建网络
net_myseq = MySequential(
nn.Linear(5, 3),
nn.ReLU(),
nn.Linear(3, 1)
)
print("自定义Sequential网络:")
print(net_myseq)
# 训练自定义Sequential网络
print("\n训练自定义Sequential网络:")
result_loss_custom = train_model(
train_dataloader,
net=net_myseq,
lr=0.01,
epoch=300
)
# 测试预测
output = net_myseq(X_eval)
print(f"\n预测输出形状: {output.shape}")
print(f"网络参数数量: {sum(p.numel() for p in net_myseq.parameters())}")
6. 自定义参数与固定模块
6.1 固定参数模块
有时候我们需要某些参数不参与训练,这可以通过设置requires_grad=False实现。
python
# ==================== 固定参数模块 ====================
class Fixed_Module(nn.Module):
"""
包含固定参数的模块
- gamma矩阵:固定参数,不参与训练
- while循环:实现动态计算图
"""
def __init__(self):
super().__init__()
# 固定参数:不参与梯度计算
self.gamma = torch.randn(size=(3, 3), requires_grad=False)
# 可训练参数
self.linear1 = nn.Linear(5, 3)
self.linear2 = nn.Linear(3, 1)
def forward(self, X):
# 第一层
X = self.linear1(X)
# 使用固定矩阵进行变换
X = self.linear2(torch.mm(X, self.gamma))
# 动态while循环:计算图会根据条件展开
while X.sum() > 10:
X = X / 2
return X
# ==================== 测试固定参数模块 ====================
fixed_module = Fixed_Module()
print("固定参数模块:")
print(fixed_module)
print("\n参数状态:")
for name, param in fixed_module.named_parameters():
print(f" {name}: requires_grad={param.requires_grad}, shape={param.shape}")
# 训练固定参数模块
print("\n训练固定参数模块:")
result_loss_fixed = train_model(
train_dataloader,
net=fixed_module,
lr=0.01,
epoch=300
)
7. 自定义损失函数
7.1 可学习权重的组合损失
自定义损失函数,损失函数的权重也可以作为可学习参数。
python
# ==================== 自定义组合损失函数 ====================
class LearnableCombinedLoss(nn.Module):
"""
可学习权重的组合损失函数
- gamma, beta:可学习的权重参数
- loss1, loss2:基础损失函数
"""
def __init__(self,
loss1=nn.MSELoss(reduction="mean"),
loss2=nn.L1Loss(reduction="mean")):
super().__init__()
# 可学习的权重参数
self.gamma = nn.Parameter(torch.ones(1))
self.beta = nn.Parameter(torch.ones(1))
# 基础损失函数
self.loss1 = loss1 # MSE损失
self.loss2 = loss2 # L1损失
def forward(self, pred, target):
# 计算两个损失
l1 = self.loss1(pred, target)
l2 = self.loss2(pred, target)
# 组合损失
total_loss = self.gamma * l1 + self.beta * l2
return total_loss
# ==================== 使用自定义损失函数 ====================
# 创建自定义损失
combined_loss = LearnableCombinedLoss()
# 拼接模型参数和损失函数参数
all_params = list(net.parameters()) + list(combined_loss.parameters())
# 定义优化器:为不同参数设置不同学习率
optimizer = torch.optim.Adam([
{"params": net.parameters(), "lr": 1e-3}, # 模型参数学习率
{"params": combined_loss.parameters(), "lr": 1e-2} # 损失权重学习率
])
print("自定义组合损失函数:")
print(combined_loss)
print("\n可训练参数:")
print(f" gamma: {combined_loss.gamma.item():.4f}")
print(f" beta: {combined_loss.beta.item():.4f}")
8. 参数访问与模块管理
8.1 模块访问方式
展示如何访问Sequential中的模块和参数。
python
# ==================== 模块访问 ====================
net_d = nn.Sequential(
nn.Linear(5, 3),
nn.ReLU(),
nn.Linear(3, 1)
)
print("网络结构:")
print(net_d)
print("\n=== 模块访问方式 ===")
# 方式1:遍历模块
print("\n方式1:遍历所有模块")
for m in net_d:
print(f" {m.__class__.__name__}")
# 方式2:通过索引访问
print("\n方式2:通过索引访问模块")
print(f" 第一层: {net_d[0]}")
print(f" 第二层: {net_d[1]}")
print(f" 第三层: {net_d[2]}")
# 方式3:访问模块参数
print("\n方式3:访问模块参数")
print(f" 第一层状态字典: {net_d[0].state_dict()}")
# 方式4:访问网络所有参数
print("\n方式4:访问网络所有参数")
print(" 网络状态字典:")
for name, param in net_d.state_dict().items():
print(f" {name}: shape={param.shape}")
8.2 参数数据访问
展示如何访问参数的值和梯度。
python
# ==================== 参数数据访问 ====================
print("\n=== 参数数据访问方式 ===")
# 方式1:使用.data属性
print("\n方式1:使用.data属性")
print(f" 权重: {net_d[0].weight.data[:3, :3]}")
# 方式2:使用.detach()方法
print("\n方式2:使用.detach()方法")
print(f" 权重: {net_d[0].weight.detach()[:3, :3]}")
# 方式3:查看命名参数
print("\n方式3:查看所有命名参数")
for name, param in net_d.named_parameters():
print(f" {name}: shape={param.shape}, requires_grad={param.requires_grad}")
# 方式4:查看命名模块
print("\n方式4:查看所有命名模块")
for name, module in net_d.named_modules():
if name: # 跳过根模块(空名称)
print(f" {name}: {module.__class__.__name__}")
9. 动态添加模块
9.1 动态构建网络
使用add_module方法动态添加网络模块。
python
# ==================== 动态模块定义 ====================
def block2():
"""定义一个基础块"""
return nn.Sequential(
nn.Linear(5, 5),
nn.ReLU(),
nn.Linear(5, 5)
)
def block1(times):
"""
动态构建多个基础块
使用add_module方法为每个模块命名
"""
net = nn.Sequential()
for i in range(times):
# 使用add_module动态添加模块
net.add_module(f"module{i}", block2())
return net
# ==================== 构建动态网络 ====================
# 构建5个基础块的网络
net_dynamic = nn.Sequential(
block1(5), # 5个基础块
nn.Linear(5, 1) # 输出层
)
print("动态构建的网络:")
print(net_dynamic)
print("\n网络参数:")
for name, param in net_dynamic.named_parameters():
print(f" {name}: shape={param.shape}")
10. 参数手动修改与梯度陷阱
10.1 手动修改参数的陷阱
展示手动修改参数可能导致的梯度计算错误。
python
# ==================== 梯度陷阱演示 ====================
# 创建简单的线性网络
net = torch.nn.Linear(1, 1, bias=False)
net.weight.data = torch.tensor([[1.0]]) # 初始化权重为1.0
# 定义优化器
opt = torch.optim.SGD(net.parameters(), lr=0.1)
# 准备数据
x = torch.tensor([[2.0]])
y_true = torch.tensor([[3.0]])
# 前向传播
y_pred = net(x) # θ=1.0,y_pred=2.0
loss2 = nn.MSELoss()
print("=== 参数修改前 ===")
print(f" 初始权重: {net.weight.data.item():.4f}")
print(f" 预测值: {y_pred.item():.4f}")
# 计算损失
loss = loss2(y_pred, y_true)
print(f" 初始损失: {loss.item():.4f}")
# ==================== 关键操作:手动修改参数 ====================
# 这里的修改会导致梯度计算错误!
net.weight.data.zero_() # 将权重改为0.0
print("\n=== 手动修改参数后 ===")
print(f" 修改后权重: {net.weight.data.item():.4f}")
print(f" 注意:此时梯度是基于修改前的参数计算的!")
# 反向传播
opt.zero_grad()
loss.backward()
print(f" 计算的梯度: {net.weight.grad.item():.4f}")
# 参数更新
opt.step()
print(f" 更新后权重: {net.weight.data.item():.4f}")
print("\n=== 原理说明 ===")
print(" 正确梯度应该是:-0.1 * 4 * (1.0*2-3) = 0.4")
print(" 但因为手动修改了参数,实际梯度计算基于错误的值")
11. 参数初始化方法
11.1 内置初始化方法
PyTorch提供了多种参数初始化方法。
python
# ==================== 参数初始化方法 ====================
def init_normal(m):
"""正态分布初始化"""
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=1, std=1)
nn.init.zeros_(m.bias)
def init_constant(m):
"""常数初始化"""
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
def init_uniform(m):
"""均匀分布初始化"""
if type(m) == nn.Linear:
nn.init.uniform_(m.weight, -10, 10)
nn.init.zeros_(m.bias)
def init_xavier(m):
"""Xavier初始化"""
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
nn.init.zeros_(m.bias)
def init_kaiming(m):
"""Kaiming初始化:适合ReLU激活函数"""
if type(m) == nn.Linear:
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
nn.init.zeros_(m.bias)
# ==================== 自定义初始化 ====================
def init_custom(m):
"""自定义初始化:只保留绝对值大于5的值"""
if type(m) == nn.Linear:
nn.init.uniform_(m.weight, -10, 10)
# 只保留绝对值>=5的值,其他设为0
m.weight.data *= (m.weight.data.abs() >= 5).float()
nn.init.zeros_(m.bias)
# ==================== 测试不同初始化方法 ====================
linear_net = nn.Linear(1, 1, bias=False)
# 应用自定义初始化
init_custom(linear_net)
print("自定义初始化结果:")
print(f" 权重值: {linear_net.weight.data.item():.4f}")
# 多次应用初始化并统计分布
uniform_list = []
for i in range(1000):
linear_net.apply(init_custom)
uniform_list.append(int(linear_net.weight.data.item()))
# 绘制分布直方图
plt.figure(figsize=(10, 6))
plt.hist(uniform_list, bins=20, edgecolor='black', alpha=0.7)
plt.xlabel('Weight Value')
plt.ylabel('Frequency')
plt.title('Custom Initialization Distribution')
plt.grid(True, alpha=0.3)
plt.show()
print(f"前5个值: {uniform_list[:5]}")
12. 深度自编码器实现
12.1 自编码器原理
自编码器是一种无监督学习方法,用于学习数据的压缩表示。
python
# ==================== 模拟高维数据 ====================
torch.manual_seed(42)
def simulate_data(num_sample, features):
"""
模拟高维数据
- 每个特征有不同的均值和标准差
"""
mean = torch.rand(features) * 10 - 5
std = torch.randint(low=1, high=10, size=(features,))
# 为每个特征生成正态分布数据
normal_list = [
torch.normal(mean=mean1, std=std1, size=(num_sample,))
for mean1, std1 in zip(mean, std)
]
return torch.stack(normal_list, dim=1)
# 生成1000个样本,20个特征
X = simulate_data(1000, (20,))
print(f"数据形状: {X.shape}")
12.2 自编码器网络构建
构建编码器和解码器网络。
python
# ==================== 编码器定义 ====================
left_side_layer = nn.Sequential(
nn.Linear(20, 15),
nn.ReLU(),
nn.Linear(15, 15),
nn.ReLU(),
nn.Linear(15, 10) # 压缩到10维
)
# ==================== 解码器定义 ====================
def right_side(layer):
"""从编码器层提取权重和偏置,用于构建解码器"""
dict_1 = {}
weight = layer.weight.detach().T # 转置权重
bias = layer.bias.detach()
dict_1['weight'] = weight
dict_1['bias'] = bias
return dict_1
class Rightside(nn.Module):
"""
解码器网络
- 自动从编码器构建对称的解码器
- 使用编码器的转置权重
"""
def __init__(self, left_side_net):
super().__init__()
# 提取编码器的Linear层
module_list = []
for i in range(len(left_side_net)):
if isinstance(left_side_net[i], nn.Linear):
module_list.append(left_side_net[i])
# 反转顺序构建解码器
index_list = list(range(len(module_list)))[::-1]
right_dict = {}
for i, k in enumerate(index_list):
right_dict[i] = right_side(module_list[k])
self.right_dict = right_dict
def forward(self, X):
# 逐层解码
for layer in self.right_dict.values():
X = torch.matmul(X, layer['weight'].T) # 使用转置权重
return X
# ==================== 构建完整自编码器 ====================
right_side_net = Rightside(left_side_layer)
net = nn.Sequential(left_side_layer, right_side_net)
print("自编码器网络:")
print(net)
print("\n参数梯度状态:")
for name, param in net.named_parameters():
print(f" {name}: requires_grad={param.requires_grad}")
12.3 自编码器训练
训练自编码器进行数据重建。
python
# ==================== 训练自编码器 ====================
# 定义损失函数和优化器
criterion = nn.MSELoss(reduction='mean')
optimizer = torch.optim.SGD(net.parameters(), lr=1e-1)
result_list = []
print("开始训练自编码器...")
for i in tqdm(range(30000), desc="Autoencoder Training"):
optimizer.zero_grad()
# 前向传播
Y = net(X)
# 计算重建损失
loss_value = criterion(X, Y)
# 反向传播
loss_value.backward()
optimizer.step()
result_list.append(loss_value.item())
# ==================== 绘制训练曲线 ====================
plt.figure(figsize=(10, 6))
plt.plot(result_list, linewidth=2)
plt.xlabel('Iteration')
plt.ylabel('Reconstruction Loss')
plt.title('Autoencoder Training Progress')
plt.yscale('log') # 对数坐标
plt.grid(True, alpha=0.3)
plt.show()
print(f"最终重建损失: {result_list[-1]:.6f}")
12.4 保存和加载模型
python
# ==================== 保存模型 ====================
torch.save(net.state_dict(), "autoencoder_model.pth")
print("模型已保存到: autoencoder_model.pth")
# ==================== 加载模型 ====================
# 创建新的网络实例
net_loaded = nn.Sequential(
nn.Linear(20, 15),
nn.ReLU(),
nn.Linear(15, 15),
nn.ReLU(),
nn.Linear(15, 10),
Rightside(left_side_layer)
)
# 加载参数
net_loaded.load_state_dict(torch.load("autoencoder_model.pth"))
print("\n模型参数已加载")
# 验证加载的模型
with torch.no_grad():
Y_loaded = net_loaded(X)
reconstruction_error = criterion(X, Y_loaded)
print(f"加载模型的重建误差: {reconstruction_error.item():.6f}")
13. detach与梯度分离
13.1 detach的作用
detach()用于从计算图中分离张量,停止梯度传播。
python
# ==================== detach演示 ====================
torch.manual_seed(11)
# 创建需要梯度的张量
x1 = torch.rand((10, 1), requires_grad=True)
print("=== 原始张量 x1 ===")
print(f" 值(前5个): {x1[:5].squeeze().tolist()}")
print(f" requires_grad: {x1.requires_grad}")
print(f" 梯度: {x1.grad}")
# 使用detach分离张量
x2 = x1.detach()
print("\n=== 分离后的张量 x2 ===")
print(f" 值(前5个): {x2[:5].squeeze().tolist()}")
print(f" requires_grad: {x2.requires_grad}")
# 修改分离后的张量
x2[0, 0] = 1
print("\n=== 修改x2后 ===")
print(f" x1值(前5个): {x1[:5].squeeze().tolist()}")
print(f" x2值(前5个): {x2[:5].squeeze().tolist()}")
print(f" x1 requires_grad: {x1.requires_grad}")
print(f" x2 requires_grad: {x2.requires_grad}")
# 测试梯度传播
y1 = x1 * 2
y2 = x2 * 2
print("\n=== 梯度计算测试 ===")
print(f" y1 requires_grad: {y1.requires_grad}")
print(f" y2 requires_grad: {y2.requires_grad}")
# 反向传播
y1.sum().backward()
print(f"\n反向传播后 x1 的梯度: {x1.grad}")
print(f"x2 没有梯度,因为它不在计算图中")
13.2 detach的应用场景
python
# ==================== detach应用场景 ====================
# 场景1:GAN训练中的生成器
# 生成器生成的图像需要detach后传入判别器
class SimpleGAN(nn.Module):
def __init__(self):
super().__init__()
self.generator = nn.Sequential(
nn.Linear(10, 20),
nn.ReLU()
)
self.discriminator = nn.Sequential(
nn.Linear(20, 1),
nn.Sigmoid()
)
def train_step(self, noise, real_labels, fake_labels):
# 生成假数据
fake_data = self.generator(noise)
# 关键:detach停止梯度,避免更新生成器
fake_pred = self.discriminator(fake_data.detach())
# 训练判别器
real_pred = self.discriminator(real_labels)
return fake_pred, real_pred
# 场景2:Actor-Critic算法
# 计算优势函数时需要detach策略网络的输出
def compute_advantages(rewards, values, gamma=0.99):
"""
计算优势函数
- values: 来自价值网络,需要detach
"""
# detach确保不会反向传播到价值网络
returns = rewards + gamma * values.detach()
advantages = returns - values.detach()
return advantages
# 场景3:预训练模型微调
# 冻结某些层时使用detach
def freeze_layers(model, layer_names):
"""冻结指定层"""
for name, param in model.named_parameters():
if any(layer_name in name for layer_name in layer_names):
param.requires_grad = False
print(f"冻结层: {name}")
print("detach的典型应用场景:")
print("1. GAN训练:生成器输出detach后传入判别器")
print("2. 强化学习:优势函数计算时detach策略网络输出")
print("3. 迁移学习:冻结预训练层时使用detach")