PyTorch实现一元二次函数拟合
本教程将介绍如何使用PyTorch实现一元二次函数的拟合。通过这个简单的例子,我们可以学习PyTorch的基本使用方法,包括模型定义、损失函数、优化器以及可视化训练过程。
目录
步骤 1: 生成离散点
1.1 功能说明
在本步骤中,我们将生成一些带噪声的离散数据点。通过设置一个确定的随机种子,确保结果的可复现性。生成的 x 数据范围从 -5 到 5,y 数据为一元二次函数的结果,并且加入了随机噪声。
1.2 函数解析
- 作用:生成数据用于训练模型,模拟实际数据采集过程中的噪声。
- 背景:数据生成是机器学习中的基础步骤,通过合成的数据来测试模型的有效性和精度。
- 参数:
np.random.seed(0)
: 设置随机种子为0,确保每次运行代码时生成相同的随机数据。np.random.uniform(-5, 5, 100)
: 生成100个从-5到5之间均匀分布的随机数。true_a, true_b, true_c
: 设置一元二次函数的真实参数,用于生成y值。
- 返回值:
x_train
、y_train
: 这两个变量是训练模型的输入和目标输出,形式为PyTorch张量。
带详细中文注释的代码
# 设置随机种子,确保结果可复现
np.random.seed(0)
# 生成100个x值,范围从-5到5
x = np.random.uniform(-5, 5, 100)
# 设置一元二次函数的真实参数值
true_a, true_b, true_c = 3.0, 2.0, 1.0
# 根据真实的函数和添加噪声生成y值
y = true_a * x**2 + true_b * x + true_c + np.random.randn(100) * 10
# 将x和y转换为PyTorch张量
x_train = torch.tensor(x, dtype=torch.float32).view(-1, 1)
y_train = torch.tensor(y, dtype=torch.float32).view(-1, 1)
关键函数详解
-
np.random.seed(0)
- 作用:设置随机数生成器的种子值。
- 在本例中:确保每次运行代码时生成的随机数序列相同,从而保证实验结果的可复现性。
-
np.random.uniform(-5, 5, 100)
- 作用:生成100个在-5到5之间均匀分布的随机数。
- 在本例中:模拟不同的输入值,这些输入将作为模型的特征。
-
np.random.randn(100) * 10
- 作用:生成100个标准正态分布的随机数,并乘以10放大噪声。
- 在本例中:模拟真实世界中的数据噪声,测试模型在有噪声情况下的鲁棒性。
总结
本步骤的目标是生成带噪声的离散数据,用于后续的模型训练。在实际应用中,通常会使用真实数据替代这些合成数据,但此步骤帮助我们在没有真实数据时进行模型的验证和调试。
步骤 2: 定义模型
2.1 功能说明
在本步骤中,我们定义了一个简单的一元二次函数模型。通过定义模型结构并初始化可训练的参数,使得模型能够根据输入数据拟合出目标输出。
2.2 函数解析
- 作用:创建一个继承自
torch.nn.Module
的类来定义一元二次模型,并初始化参数。 - 背景:深度学习模型通常通过定义网络结构来进行训练和预测,本例中使用了一个简单的二次函数模型。
- 参数:
torch.nn.Parameter(torch.tensor([0.5]))
: 用来定义模型的可训练参数,这些参数的初值接近但不等于真实值。
- 返回值:
- 返回模型本身,通过模型的
forward
方法来进行前向传播计算。
- 返回模型本身,通过模型的
带详细中文注释的代码
class QuadraticModel(torch.nn.Module):
"""定义一元二次函数模型"""
def __init__(self):
"""初始化模型参数"""
super(QuadraticModel, self).__init__()
# 初始化参数a, b, c,初始值接近真实值
self.a = torch.nn.Parameter(torch.tensor([0.5]))
self.b = torch.nn.Parameter(torch.tensor([0.5]))
self.c = torch.nn.Parameter(torch.tensor([0.0]))
def forward(self, x):
"""定义前向传播函数"""
return self.a * x**2 + self.b * x + self.c
关键函数详解
-
class QuadraticModel(torch.nn.Module)
- 作用:定义一个继承自
torch.nn.Module
的模型类。 - 在本例中:通过继承
torch.nn.Module
,我们可以使用PyTorch提供的自动化功能(如梯度计算、参数更新等)。
- 作用:定义一个继承自
-
self.a = torch.nn.Parameter(torch.tensor([0.5]))
- 作用:定义模型的参数 a,并将其包装为一个可训练的参数。
- 在本例中:通过将参数 a 定义为
torch.nn.Parameter
类型,PyTorch 会自动将其加入到模型的可训练参数列表中。
-
def forward(self, x)
- 作用:定义模型的前向传播函数。
- 在本例中:根据输入的 x 值计算预测值 y。
总结
此步骤中,我们通过定义模型类来实现了对一元二次函数的拟合。创建模型后,我们可以通过训练来优化模型中的参数,使其尽量接近真实函数的参数。
步骤 3: 定义损失函数和优化器
3.1 功能说明
本步骤中,我们定义了损失函数和优化器。损失函数用于衡量模型预测值与真实值之间的差距,优化器则通过反向传播来更新模型参数。
3.2 函数解析
- 作用:通过定义损失函数和优化器,来优化模型的参数。
- 背景:在训练过程中,通过损失函数计算出误差,并利用优化器来调整模型参数,逐步减少误差。
- 参数:
torch.nn.MSELoss()
: 均方误差损失函数,用于回归问题。torch.optim.Adam()
: Adam优化器,常用的优化算法。torch.optim.lr_scheduler.ReduceLROnPlateau()
: 学习率调整器,用于动态调整学习率。
- 返回值:
- 定义了优化过程所需的损失函数和优化器。
带详细中文注释的代码
# 定义模型实例
model = QuadraticModel()
# 定义均方误差损失函数
criterion = torch.nn.MSELoss()
# 使用Adam优化器,学习率初始为0.005
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)
# 定义学习率调度器,当损失不再减少时调整学习率
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=50, factor=0.5)
关键函数详解
-
torch.nn.MSELoss()
- 作用:用于回归问题的损失函数,计算模型预测值与真实值之间的均方误差。
- 在本例中:通过最小化这个损失函数,模型可以学习到更准确的预测。
-
torch.optim.Adam()
- 作用:一种常用的优化算法,结合了动量和自适应学习率的优点。
- 在本例中:Adam优化器能够更快地收敛,并能避免传统梯度下降法中的学习率选择问题。
-
torch.optim.lr_scheduler.ReduceLROnPlateau()
- 作用:当损失不再减少时,动态调整学习率。
- 在本例中:学习率调度器有助于在训练过程中动态地调整学习率,以应对模型训练中的不同阶段。
总结
通过定义损失函数和优化器,本步骤实现了模型训练的基础配置。损失函数衡量误差,优化器通过反向传播更新参数,学习率调度器动态调整学习率,提高训练效率。
步骤 4: 训练模型
4.1 功能说明
在本步骤中,我们通过训练模型来优化其参数,同时记录每次训练过程中的参数变化和损失,以便后续可视化。
4.2 函数解析
- 作用:通过训练过程逐步优化模型参数,同时记录训练过程中的重要数据以供后续分析。
- 背景:训练模型是深度学习中的核心任务,训练过程中需要不断地计算损失并更新参数。
- 参数:
optimizer.zero_grad()
: 清空当前的梯度。loss.backward()
: 计算梯度。optimizer.step()
: 根据计算的梯度更新模型参数。scheduler.step(loss)
: 根据当前损失值调整学习率。
- 返回值:
- 记录训练过程中的参数和损失,用于后续的分析和可视化。
带详细中文注释的代码
# 训练模型
for epoch in range(epochs):
model.train()
optimizer.zero_grad() # 清空梯度
y_pred = model(x_train) # 前向传播
loss = criterion(y_pred, y_train) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 更新参数
scheduler.step(loss) # 更新学习率
# 每隔一定轮次记录数据
if epoch % record_interval == 0:
history['a'].append(model.a.item())
history['b'].append(model.b.item())
history['c'].append(model.c.item())
history['loss'].append(loss.item())
history['lr'].append(optimizer.param_groups[0]['lr'])
关键函数详解
-
optimizer.zero_grad()
- 作用:清空梯度。
- 在本例中:在每次反向传播之前,我们需要清空上一次迭代计算的梯度,否则会出现梯度累积的情况,导致模型参数更新不准确。
-
loss.backward()
- 作用:计算梯度。
- 在本例中:反向传播是神经网络训练的核心步骤。通过损失函数的计算结果,自动计算每个参数的梯度。
-
optimizer.step()
- 作用:更新模型参数。
- 在本例中:通过计算出的梯度,优化器更新模型的参数,使得模型的预测值更接近真实值。
总结
在训练过程中,我们通过反向传播和优化器来更新模型的参数。记录每个epoch的参数和损失,是为了后续可视化训练过程中的变化。
步骤 5: 可视化训练过程
5.1 功能说明
在本步骤中,我们将通过 matplotlib 创建动态图形,展示模型在训练过程中的拟合曲线、损失曲线、参数变化和学习率变化。这有助于直观地观察训练过程中的各项指标,确保模型训练过程的稳定性与有效性。
5.2 函数解析
- 作用:通过动态图形实时展示模型训练过程中的关键参数变化。
- 背景:在模型训练过程中,监控损失值、参数变化和学习率等指标对于评估训练效果至关重要。通过图形化的方式,可以直观地观察训练是否稳定,并进行相应的调整。
- 参数:
FuncAnimation
:用于创建动态更新的图形,每一帧展示训练过程中的一个步骤。update(frame)
:更新图形数据,每一帧更新拟合曲线、损失曲线等。
- 返回值:
- 动态图形,实时展示训练过程的变化。
带详细中文注释的代码
# 创建2x2的子图布局
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(14, 10)) # 创建2行2列的子图
plt.subplots_adjust(hspace=0.4, wspace=0.3) # 调整子图间距
# 设置子图1:拟合曲线
scatter = ax1.scatter(x, y, alpha=0.5, label='原始数据点') # 绘制原始数据点
line, = ax1.plot([], [], 'r-', linewidth=2, label='拟合曲线') # 创建拟合曲线对象,初始为空
true_line, = ax1.plot(x_plot, true_a * x_plot**2 + true_b * x_plot + true_c, 'b--',
alpha=0.7, label='真实曲线') # 绘制真实曲线
ax1.set_xlim(-5, 5) # 设置x轴范围
ax1.set_ylim(min(y)-10, max(y)+10) # 设置y轴范围
ax1.set_xlabel('x') # 设置x轴标签
ax1.set_ylabel('y') # 设置y轴标签
ax1.set_title('一元二次函数拟合过程') # 设置子图标题
ax1.legend(loc='upper left') # 显示图例,位置在左上角
ax1.grid(True) # 显示网格线
# 设置子图2:损失曲线 - 使用普通坐标轴而不是对数坐标轴
loss_line, = ax2.plot([], [], 'b-', linewidth=2) # 创建损失曲线对象,初始为空
ax2.set_xlim(0, len(history['loss'])) # 设置x轴范围
max_loss = max(history['loss']) # 计算最大损失值
min_loss = min(history['loss']) # 计算最小损失值
if np.isfinite(max_loss) and np.isfinite(min_loss): # 如果损失值有效
ax2.set_ylim(min_loss*0.9, max_loss*1.1) # 设置y轴范围,留出一些余量
else: # 如果损失值无效
ax2.set_ylim(0.1, 1000) # 设置默认的y轴范围
ax2.set_xlabel(f'训练次数 (x{record_interval})') # 设置x轴标签
ax2.set_ylabel('损失值') # 设置y轴标签
ax2.set_title('损失函数变化曲线') # 设置子图标题
ax2.grid(True) # 显示网格线
# 设置子图3:参数变化
a_line, = ax3.plot([], [], 'r-', linewidth=2, label='参数a') # 创建参数a曲线对象,初始为空
b_line, = ax3.plot([], [], 'g-', linewidth=2, label='参数b') # 创建参数b曲线对象,初始为空
c_line, = ax3.plot([], [], 'b-', linewidth=2, label='参数c') # 创建参数c曲线对象,初始为空
# 绘制真实参数的水平参考线
ax3.axhline(y=true_a, color='r', linestyle='--', alpha=0.5, label=f'真实a={true_a}')
ax3.axhline(y=true_b, color='g', linestyle='--', alpha=0.5, label=f'真实b={true_b}')
ax3.axhline(y=true_c, color='b', linestyle='--', alpha=0.5, label=f'真实c={true_c}')
ax3.set_xlim(0, len(history['a'])) # 设置x轴范围
# 设置y轴范围,确保能显示所有参数值
ax3.set_ylim(min(min(history['a']), min(history['b']), min(history['c']))-0.5,
max(max(history['a']), max(history['b']), max(history['c']))+0.5)
ax3.set_xlabel(f'训练次数 (x{record_interval})') # 设置x轴标签
ax3.set_ylabel('参数值') # 设置y轴标签
ax3.set_title('参数变化曲线') # 设置子图标题
ax3.legend(loc='upper left') # 显示图例,位置在左上角
ax3.grid(True) # 显示网格线
# 设置子图4:学习率变化 - 使用普通坐标轴而不是对数坐标轴
lr_line, = ax4.plot([], [], 'g-', linewidth=2) # 创建学习率曲线对象,初始为空
ax4.set_xlim(0, len(history['lr'])) # 设置x轴范围
ax4.set_ylim(0, max(history['lr'])*1.1) # 设置y轴范围,从0开始
ax4.set_xlabel(f'训练次数 (x{record_interval})') # 设置x轴标签
ax4.set_ylabel('学习率') # 设置y轴标签
ax4.set_title('学习率变化曲线') # 设置子图标题
ax4.grid(True) # 显示网格线
# 创建标题
fig.suptitle('一元二次函数拟合训练过程可视化', fontsize=16) # 设置总标题
# 定义动画更新函数
def update(frame):
"""动画更新函数,每帧调用一次"""
# 更新拟合曲线
a = history['a'][frame] # 获取当前帧的参数a
b = history['b'][frame] # 获取当前帧的参数b
c = history['c'][frame] # 获取当前帧的参数c
y_fit = a * x_plot**2 + b * x_plot + c # 计算当前参数下的拟合曲线
line.set_data(x_plot, y_fit) # 更新拟合曲线数据
# 更新损失曲线
x_loss = list(range(frame + 1)) # 创建x轴数据
y_loss = history['loss'][:frame + 1] # 获取损失历史数据
loss_line.set_data(x_loss, y_loss) # 更新损失曲线数据
# 更新参数曲线
x_params = list(range(frame + 1)) # 创建x轴数据
y_a = history['a'][:frame + 1] # 获取参数a历史数据
y_b = history['b'][:frame + 1] # 获取参数b历史数据
y_c = history['c'][:frame + 1] # 获取参数c历史数据
a_line.set_data(x_params, y_a) # 更新参数a曲线数据
b_line.set_data(x_params, y_b) # 更新参数b曲线数据
c_line.set_data(x_params, y_c) # 更新参数c曲线数据
# 更新学习率曲线
y_lr = history['lr'][:frame + 1] # 获取学习率历史数据
lr_line.set_data(x_params, y_lr) # 更新学习率曲线数据
# 更新标题显示当前参数和进度
progress = (frame + 1) / len(history['a']) * 100 # 计算进度百分比
# 更新总标题,显示进度和当前参数值
fig.suptitle(f'一元二次函数拟合 - 进度: {progress:.1f}% - a={a:.4f}, b={b:.4f}, c={c:.4f}', fontsize=14)
# 返回所有需要更新的图形对象
return line, loss_line, a_line, b_line, c_line, lr_line
# 创建动画
frames = len(history['a']) # 总帧数等于历史记录长度
print(f"正在创建动画,共{frames}帧...")
# 创建FuncAnimation对象,指定图形、更新函数、帧数等参数
ani = FuncAnimation(fig, update, frames=frames, blit=True, interval=50) # interval=50表示每帧间隔50毫秒
plt.tight_layout(rect=[0, 0, 1, 0.95]) # 调整布局,为顶部标题留出空间
plt.show() # 显示动画
完整代码
以下是完整的代码实现:
import torch # 导入PyTorch库,用于构建和训练神经网络
import numpy as np # 导入NumPy库,用于数值计算
import matplotlib.pyplot as plt # 导入Matplotlib库,用于数据可视化
from matplotlib.animation import FuncAnimation # 导入FuncAnimation类,用于创建动画
import matplotlib as mpl # 导入Matplotlib库的主模块
# 设置matplotlib参数,解决字体和负号显示问题
mpl.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体为SimHei,用于正常显示中文
mpl.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 使用ASCII负号而不是Unicode负号,避免字体警告
mpl.rcParams['text.usetex'] = False # 禁用TeX渲染,使用普通文本渲染
# 设置字体,避免缺少字形的警告
plt.rcParams['mathtext.default'] = 'regular' # 使用常规字体渲染数学文本
# 设置matplotlib后端为TkAgg,解决在PyCharm中的兼容性问题
import matplotlib
matplotlib.use('TkAgg') # 设置图形后端为TkAgg,兼容PyCharm
# 禁用字体相关警告
import warnings # 导入warnings模块,用于控制警告信息
warnings.filterwarnings("ignore", category=UserWarning, module="matplotlib") # 忽略matplotlib的警告
# Step 1: 生成离散点
np.random.seed(0) # 设置随机种子为0,确保结果可复现
x = np.random.uniform(-5, 5, 100) # 随机生成100个x值,范围从-5到5
true_a, true_b, true_c = 3.0, 2.0, 1.0 # 设置真实参数值:a=3, b=2, c=1
y = true_a * x**2 + true_b * x + true_c + np.random.randn(100) * 10 # 生成带噪声的y值:y = 3x^2 + 2x + 1 + 噪声
# 转换为 PyTorch 张量,便于后续训练
x_train = torch.tensor(x, dtype=torch.float32).view(-1, 1) # 将x转换为PyTorch张量,形状为[100, 1]
y_train = torch.tensor(y, dtype=torch.float32).view(-1, 1) # 将y转换为PyTorch张量,形状为[100, 1]
# Step 2: 定义模型(拟合一元二次函数 y = ax^2 + bx + c)
class QuadraticModel(torch.nn.Module):
"""定义一个一元二次函数模型,继承自torch.nn.Module"""
def __init__(self):
"""初始化模型参数"""
super(QuadraticModel, self).__init__() # 调用父类初始化方法
# 使用更好的初始化值,接近但不等于真实值
self.a = torch.nn.Parameter(torch.tensor([0.5])) # 创建可训练参数a,初始值为0.5
self.b = torch.nn.Parameter(torch.tensor([0.5])) # 创建可训练参数b,初始值为0.5
self.c = torch.nn.Parameter(torch.tensor([0.0])) # 创建可训练参数c,初始值为0.0
def forward(self, x):
"""定义前向传播函数,计算y = ax^2 + bx + c"""
return self.a * x**2 + self.b * x + self.c # 返回一元二次函数的值
# Step 3: 定义损失函数和优化器
model = QuadraticModel() # 创建模型实例
criterion = torch.nn.MSELoss() # 使用均方误差作为损失函数
# 使用学习率调度器,提高训练效率
optimizer = torch.optim.Adam(model.parameters(), lr=0.005) # 使用Adam优化器,初始学习率为0.005
# 禁用学习率调度器的verbose输出
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, # 要调整学习率的优化器
'min', # 模式:当监控值停止下降时降低学习率
patience=50, # 容忍多少个epoch没有改善
factor=0.5, # 学习率衰减因子
verbose=False # 不打印学习率变化信息
)
# 用于存储训练过程中的参数和损失
history = {
'a': [], # 存储参数a的历史值
'b': [], # 存储参数b的历史值
'c': [], # 存储参数c的历史值
'loss': [], # 存储损失值的历史
'lr': [] # 存储学习率的历史
}
# 准备用于动画的数据
epochs = 1500 # 设置训练轮数为1500
record_interval = 5 # 每5个epoch记录一次参数,用于动画显示
x_plot = np.linspace(-5, 5, 1000).reshape(-1, 1) # 生成1000个均匀分布的x值,用于绘制平滑曲线
x_plot_tensor = torch.tensor(x_plot, dtype=torch.float32) # 转换为PyTorch张量
# 禁用PyTorch的警告
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="torch") # 忽略PyTorch的警告
# Step 4: 训练模型并记录参数
print("开始训练模型...")
for epoch in range(epochs): # 循环训练epochs次
model.train() # 设置模型为训练模式
optimizer.zero_grad() # 清空梯度,避免梯度累积
y_pred = model(x_train) # 前向传播,计算预测值
loss = criterion(y_pred, y_train) # 计算损失值
# 检查损失是否为NaN或Inf
if torch.isnan(loss) or torch.isinf(loss): # 如果损失值异常
print(f"警告: 第{epoch}轮训练中损失值异常: {loss.item()}")
break # 停止训练
loss.backward() # 反向传播,计算梯度
# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 限制梯度范数不超过1.0
optimizer.step() # 更新参数
scheduler.step(loss) # 更新学习率,根据损失值调整
if epoch % record_interval == 0: # 每record_interval个epoch记录一次
# 记录当前参数和损失
history['a'].append(model.a.item()) # 记录参数a的当前值
history['b'].append(model.b.item()) # 记录参数b的当前值
history['c'].append(model.c.item()) # 记录参数c的当前值
history['loss'].append(loss.item()) # 记录损失值
history['lr'].append(optimizer.param_groups[0]['lr']) # 记录当前学习率
if epoch % 100 == 0: # 每100个epoch打印一次信息
print(f'Epoch [{epoch}/{epochs}], Loss: {loss.item():.4f}, '
f'参数: a={model.a.item():.4f}, b={model.b.item():.4f}, c={model.c.item():.4f}, '
f'学习率: {optimizer.param_groups[0]["lr"]:.6f}')
# 打印最终拟合的参数
print(f'最终拟合的参数: a={model.a.item():.4f}, b={model.b.item():.4f}, c={model.c.item():.4f}')
print(f'原始函数参数: a={true_a:.4f}, b={true_b:.4f}, c={true_c:.4f}')
# 计算并打印参数误差百分比
print(f'参数误差: a误差={(model.a.item()-true_a)/true_a*100:.2f}%, '
f'b误差={(model.b.item()-true_b)/true_b*100:.2f}%, '
f'c误差={(model.c.item()-true_c)/true_c*100:.2f}%')
# 检查是否有足够的历史记录来创建动画
if len(history['loss']) < 2: # 如果记录数少于2,无法创建动画
print("训练过程中出现问题,无法创建动画")
exit() # 退出程序
# 创建动态可视化效果
print("创建动画...")
# 创建2x2的子图布局
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(14, 10)) # 创建2行2列的子图
plt.subplots_adjust(hspace=0.4, wspace=0.3) # 调整子图间距
# 设置子图1:拟合曲线
scatter = ax1.scatter(x, y, alpha=0.5, label='原始数据点') # 绘制原始数据点
line, = ax1.plot([], [], 'r-', linewidth=2, label='拟合曲线') # 创建拟合曲线对象,初始为空
true_line, = ax1.plot(x_plot, true_a * x_plot**2 + true_b * x_plot + true_c, 'b--',
alpha=0.7, label='真实曲线') # 绘制真实曲线
ax1.set_xlim(-5, 5) # 设置x轴范围
ax1.set_ylim(min(y)-10, max(y)+10) # 设置y轴范围
ax1.set_xlabel('x') # 设置x轴标签
ax1.set_ylabel('y') # 设置y轴标签
ax1.set_title('一元二次函数拟合过程') # 设置子图标题
ax1.legend(loc='upper left') # 显示图例,位置在左上角
ax1.grid(True) # 显示网格线
# 设置子图2:损失曲线 - 使用普通坐标轴而不是对数坐标轴
loss_line, = ax2.plot([], [], 'b-', linewidth=2) # 创建损失曲线对象,初始为空
ax2.set_xlim(0, len(history['loss'])) # 设置x轴范围
max_loss = max(history['loss']) # 计算最大损失值
min_loss = min(history['loss']) # 计算最小损失值
if np.isfinite(max_loss) and np.isfinite(min_loss): # 如果损失值有效
ax2.set_ylim(min_loss*0.9, max_loss*1.1) # 设置y轴范围,留出一些余量
else: # 如果损失值无效
ax2.set_ylim(0.1, 1000) # 设置默认的y轴范围
ax2.set_xlabel(f'训练次数 (x{record_interval})') # 设置x轴标签
ax2.set_ylabel('损失值') # 设置y轴标签
ax2.set_title('损失函数变化曲线') # 设置子图标题
ax2.grid(True) # 显示网格线
# 设置子图3:参数变化
a_line, = ax3.plot([], [], 'r-', linewidth=2, label='参数a') # 创建参数a曲线对象,初始为空
b_line, = ax3.plot([], [], 'g-', linewidth=2, label='参数b') # 创建参数b曲线对象,初始为空
c_line, = ax3.plot([], [], 'b-', linewidth=2, label='参数c') # 创建参数c曲线对象,初始为空
# 绘制真实参数的水平参考线
ax3.axhline(y=true_a, color='r', linestyle='--', alpha=0.5, label=f'真实a={true_a}')
ax3.axhline(y=true_b, color='g', linestyle='--', alpha=0.5, label=f'真实b={true_b}')
ax3.axhline(y=true_c, color='b', linestyle='--', alpha=0.5, label=f'真实c={true_c}')
ax3.set_xlim(0, len(history['a'])) # 设置x轴范围
# 设置y轴范围,确保能显示所有参数值
ax3.set_ylim(min(min(history['a']), min(history['b']), min(history['c']))-0.5,
max(max(history['a']), max(history['b']), max(history['c']))+0.5)
ax3.set_xlabel(f'训练次数 (x{record_interval})') # 设置x轴标签
ax3.set_ylabel('参数值') # 设置y轴标签
ax3.set_title('参数变化曲线') # 设置子图标题
ax3.legend(loc='upper left') # 显示图例,位置在左上角
ax3.grid(True) # 显示网格线
# 设置子图4:学习率变化 - 使用普通坐标轴而不是对数坐标轴
lr_line, = ax4.plot([], [], 'g-', linewidth=2) # 创建学习率曲线对象,初始为空
ax4.set_xlim(0, len(history['lr'])) # 设置x轴范围
ax4.set_ylim(0, max(history['lr'])*1.1) # 设置y轴范围,从0开始
ax4.set_xlabel(f'训练次数 (x{record_interval})') # 设置x轴标签
ax4.set_ylabel('学习率') # 设置y轴标签
ax4.set_title('学习率变化曲线') # 设置子图标题
ax4.grid(True) # 显示网格线
# 创建标题
fig.suptitle('一元二次函数拟合训练过程可视化', fontsize=16) # 设置总标题
# 定义动画更新函数
def update(frame):
"""动画更新函数,每帧调用一次"""
# 更新拟合曲线
a = history['a'][frame] # 获取当前帧的参数a
b = history['b'][frame] # 获取当前帧的参数b
c = history['c'][frame] # 获取当前帧的参数c
y_fit = a * x_plot**2 + b * x_plot + c # 计算当前参数下的拟合曲线
line.set_data(x_plot, y_fit) # 更新拟合曲线数据
# 更新损失曲线
x_loss = list(range(frame + 1)) # 创建x轴数据
y_loss = history['loss'][:frame + 1] # 获取损失历史数据
loss_line.set_data(x_loss, y_loss) # 更新损失曲线数据
# 更新参数曲线
x_params = list(range(frame + 1)) # 创建x轴数据
y_a = history['a'][:frame + 1] # 获取参数a历史数据
y_b = history['b'][:frame + 1] # 获取参数b历史数据
y_c = history['c'][:frame + 1] # 获取参数c历史数据
a_line.set_data(x_params, y_a) # 更新参数a曲线数据
b_line.set_data(x_params, y_b) # 更新参数b曲线数据
c_line.set_data(x_params, y_c) # 更新参数c曲线数据
# 更新学习率曲线
y_lr = history['lr'][:frame + 1] # 获取学习率历史数据
lr_line.set_data(x_params, y_lr) # 更新学习率曲线数据
# 更新标题显示当前参数和进度
progress = (frame + 1) / len(history['a']) * 100 # 计算进度百分比
# 更新总标题,显示进度和当前参数值
fig.suptitle(f'一元二次函数拟合 - 进度: {progress:.1f}% - a={a:.4f}, b={b:.4f}, c={c:.4f}', fontsize=14)
# 返回所有需要更新的图形对象
return line, loss_line, a_line, b_line, c_line, lr_line
# 创建动画
frames = len(history['a']) # 总帧数等于历史记录长度
print(f"正在创建动画,共{frames}帧...")
# 创建FuncAnimation对象,指定图形、更新函数、帧数等参数
ani = FuncAnimation(fig, update, frames=frames, blit=True, interval=50) # interval=50表示每帧间隔50毫秒
plt.tight_layout(rect=[0, 0, 1, 0.95]) # 调整布局,为顶部标题留出空间
plt.show() # 显示动画
总结
通过本教程,我们学习了如何使用PyTorch实现一元二次函数的拟合。主要步骤包括:
- 生成带噪声的离散数据点
- 定义一元二次函数模型
- 设置损失函数和优化器
- 训练模型并记录参数变化
- 可视化训练过程
这个简单的例子展示了PyTorch的基本使用方法,包括模型定义、训练循环和可视化。通过这些基础知识,我们可以进一步学习更复杂的深度学习模型和应用。