PyTorch实现一元二次函数拟合

PyTorch实现一元二次函数拟合

本教程将介绍如何使用PyTorch实现一元二次函数的拟合。通过这个简单的例子,我们可以学习PyTorch的基本使用方法,包括模型定义、损失函数、优化器以及可视化训练过程。

目录

  1. 步骤 1: 生成离散点
  2. 步骤 2: 定义模型
  3. 步骤 3: 定义损失函数和优化器
  4. 步骤 4: 训练模型
  5. 步骤 5: 可视化训练过程
  6. 完整代码

步骤 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_trainy_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实现一元二次函数的拟合。主要步骤包括:

  1. 生成带噪声的离散数据点
  2. 定义一元二次函数模型
  3. 设置损失函数和优化器
  4. 训练模型并记录参数变化
  5. 可视化训练过程

这个简单的例子展示了PyTorch的基本使用方法,包括模型定义、训练循环和可视化。通过这些基础知识,我们可以进一步学习更复杂的深度学习模型和应用。

参考资料

构建一个简单的神经网络来拟合一元二次函数通常涉及以下几个步骤: 1. **数据准备**:首先需要一组输入值(x)及其对应的期望输出(y),对于一元二次函数,y = ax^2 + bx + c。你可以手动创建一些样本点,或者直接使用已知函数生成。 2. **模型选择**:我们将使用一个只有一个隐藏层的神经网络,这个隐藏层通常足够处理简单的二次函数。可以考虑使用全连接层,其中输入节点数等于x的维度,隐藏节点数根据任务复杂度调整,输出节点数为1。 3. **模型构建**:使用深度学习框架如TensorFlow或PyTorch,定义网络结构。例如,在Python中: ```python import tensorflow as tf model = tf.keras.models.Sequential([ tf.keras.layers.Dense(units=10, activation='relu', input_shape=(1,)), # 隐藏层 tf.keras.layers.Dense(units=1) # 输出层 ]) ``` 4. **编译模型**:设置损失函数(通常用均方误差MSE),优化器(如Adam)和评估指标(准确率可能不是最佳选择,因为这是回归问题): ```python model.compile(loss='mean_squared_error', optimizer=tf.optimizers.Adam()) ``` 5. **训练模型**:提供训练数据 `(X_train, y_train)` 和验证数据 `(X_val, y_val)`,运行fit()方法进行训练: ```python history = model.fit(X_train, y_train, epochs=100, validation_data=(X_val, y_val)) ``` 6. **可视化训练过程**:通过`history.history` 可以查看训练过程中损失和验证损失的变化,这可以帮助理解模型是否过拟合或欠拟合。你可以使用matplotlib绘制这些数据。 7. **预测及分析**:使用`model.predict()` 函数对新数据进行预测,并将预测结果和实际值进行对比,观察拟合效果。分析模型在不同输入区域的表现,以及误差分布情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值