1. 项目背景与核心目标
在机器学习和深度学习领域,函数拟合是一个经典的基础性任务。不同于传统的线性回归,非线性函数拟合能够更好地模拟现实世界中复杂的输入输出关系。本次我们要实现的任务是使用PyTorch框架拟合一个特定的三次多项式函数:y = x³ + 2x²。
这个看似简单的任务实际上包含了深度学习中的多个核心概念:
- 前馈神经网络的构建
- 损失函数的定义与优化
- 反向传播算法的实现
- 优化器的选择与参数调整
选择这个特定函数有几个典型原因:
- 它包含了非线性项(x³)和二次项(x²),能够很好地测试神经网络的非线性表达能力
- 相比更简单的二次函数,它需要更复杂的网络结构才能准确拟合
- 函数曲线有明显的拐点,有助于观察神经网络在不同区间的拟合表现
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与数据生成
2.1 PyTorch环境配置
在开始之前,我们需要确保正确配置了PyTorch环境。以下是推荐的安装方式:
bash复制# 使用conda安装PyTorch(推荐)
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
# 或者使用pip安装
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
注意:选择CUDA版本时,请确保与您的显卡驱动兼容。可以通过
nvidia-smi命令查看支持的CUDA版本。
2.2 训练数据生成
我们需要生成用于训练和测试的数据集。对于这个任务,我们将在[-5,5]区间内均匀采样:
python复制import torch
# 设置随机种子保证可重复性
torch.manual_seed(42)
# 生成训练数据
def generate_data(n_samples=1000):
x = torch.linspace(-5, 5, n_samples).unsqueeze(1)
y = x.pow(3) + 2 * x.pow(2)
return x, y
# 划分训练集和测试集
x_train, y_train = generate_data(800)
x_test, y_test = generate_data(200)
数据可视化是理解问题的重要步骤。我们可以使用matplotlib绘制原始函数曲线:
python复制import matplotlib.pyplot as plt
plt.figure(figsize=(10,6))
plt.plot(x_train.numpy(), y_train.numpy(), 'b-', label='True function')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Target Function: y = x³ + 2x²')
plt.legend()
plt.grid(True)
plt.show()
3. 神经网络模型设计
3.1 网络架构选择
对于这个非线性回归任务,我们需要设计一个足够强大的网络结构。经过多次实验比较,以下架构表现良好:
python复制import torch.nn as nn
class PolyNet(nn.Module):
def __init__(self, hidden_size=64):
super(PolyNet, self).__init__()
self.fc1 = nn.Linear(1, hidden_size)
self.fc2 = nn.Linear(hidden_size, hidden_size)
self.fc3 = nn.Linear(hidden_size, 1)
self.activation = nn.ReLU()
def forward(self, x):
x = self.activation(self.fc1(x))
x = self.activation(self.fc2(x))
x = self.fc3(x)
return x
这个网络包含三个全连接层:
- 输入层:1个神经元(对应输入x)
- 两个隐藏层:各64个神经元,使用ReLU激活函数
- 输出层:1个神经元(对应预测的y值)
3.2 为什么选择这个结构?
-
隐藏层数量:理论上,具有一个隐藏层的神经网络可以逼近任何连续函数。但我们选择两个隐藏层是因为:
- 更深的网络可以学习更复杂的特征表示
- 在实践中,深层网络通常比浅层网络收敛更快
-
神经元数量:64个神经元提供了足够的容量来学习三次多项式关系。太少的神经元会导致欠拟合,太多则可能引起过拟合。
-
激活函数选择:ReLU激活函数相比sigmoid或tanh有几个优势:
- 计算简单,训练速度快
- 缓解梯度消失问题
- 在实践中表现良好
4. 训练过程实现
4.1 损失函数与优化器
我们使用均方误差(MSE)作为损失函数,Adam作为优化器:
python复制model = PolyNet()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
实际测试中发现,学习率设为0.001时训练最稳定。学习率太高会导致震荡,太低则收敛缓慢。
4.2 训练循环实现
完整的训练过程包括以下步骤:
python复制def train(model, x_train, y_train, epochs=1000):
losses = []
for epoch in range(epochs):
# 前向传播
outputs = model(x_train)
loss = criterion(outputs, y_train)
# 反向传播与优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 记录损失
losses.append(loss.item())
if (epoch+1) % 100 == 0:
print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
return losses
# 开始训练
loss_history = train(model, x_train, y_train)
4.3 训练过程监控
我们可以绘制训练损失曲线来监控学习过程:
python复制plt.plot(loss_history)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Curve')
plt.grid(True)
plt.show()
理想情况下,损失应该单调递减并最终收敛到一个较小的值。如果观察到损失震荡或停滞,可能需要调整学习率或网络结构。
5. 模型评估与结果分析
5.1 测试集性能评估
训练完成后,我们需要评估模型在未见过的测试数据上的表现:
python复制with torch.no_grad():
test_outputs = model(x_test)
test_loss = criterion(test_outputs, y_test)
print(f'Test Loss: {test_loss.item():.4f}')
一个好的模型应该在测试集上表现与训练集相当。如果测试损失明显高于训练损失,可能出现了过拟合。
5.2 拟合效果可视化
将预测结果与真实函数对比:
python复制plt.figure(figsize=(10,6))
plt.plot(x_test.numpy(), y_test.numpy(), 'b-', label='True function')
plt.plot(x_test.numpy(), test_outputs.numpy(), 'r--', label='Predicted')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Function Fitting Result')
plt.legend()
plt.grid(True)
plt.show()
理想情况下,红色虚线(预测)应该与蓝色实线(真实)几乎重合。如果发现某些区间拟合不佳,可能需要调整网络结构或训练参数。
5.3 误差分析
计算并可视化预测误差:
python复制errors = test_outputs - y_test
plt.figure(figsize=(10,6))
plt.plot(x_test.numpy(), errors.numpy(), 'g-')
plt.xlabel('x')
plt.ylabel('Error')
plt.title('Prediction Errors')
plt.grid(True)
plt.show()
误差应该在零线附近随机波动。如果出现系统性偏差(如总是高估或低估),表明模型存在偏差,可能需要增加网络容量。
6. 高级技巧与优化方向
6.1 学习率调度
固定学习率有时不是最优选择。我们可以尝试使用学习率调度器:
python复制from torch.optim.lr_scheduler import StepLR
# 每200个epoch将学习率乘以0.1
scheduler = StepLR(optimizer, step_size=200, gamma=0.1)
# 在训练循环中加入
scheduler.step()
6.2 批量归一化
添加批量归一化层可以加速训练并提高稳定性:
python复制class PolyNetBN(nn.Module):
def __init__(self, hidden_size=64):
super(PolyNetBN, self).__init__()
self.fc1 = nn.Linear(1, hidden_size)
self.bn1 = nn.BatchNorm1d(hidden_size)
self.fc2 = nn.Linear(hidden_size, hidden_size)
self.bn2 = nn.BatchNorm1d(hidden_size)
self.fc3 = nn.Linear(hidden_size, 1)
self.activation = nn.ReLU()
def forward(self, x):
x = self.activation(self.bn1(self.fc1(x)))
x = self.activation(self.bn2(self.fc2(x)))
x = self.fc3(x)
return x
6.3 早停策略
为了防止过拟合,可以实现早停机制:
python复制best_loss = float('inf')
patience = 20
counter = 0
for epoch in range(epochs):
# ...训练代码...
# 早停检查
if loss.item() < best_loss:
best_loss = loss.item()
counter = 0
torch.save(model.state_dict(), 'best_model.pth')
else:
counter += 1
if counter >= patience:
print(f'Early stopping at epoch {epoch+1}')
break
7. 常见问题与解决方案
7.1 模型无法收敛
可能原因及解决方案:
- 学习率不合适:尝试调整学习率,通常在0.0001到0.01之间
- 网络结构太简单:增加隐藏层或神经元数量
- 梯度消失:使用ReLU激活函数或添加批量归一化层
7.2 过拟合问题
解决方法:
- 增加训练数据量
- 使用L2正则化:
python复制optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=0.01) - 使用dropout层:
python复制self.dropout = nn.Dropout(0.2) # 添加到网络定义中
7.3 预测结果不稳定
可能原因:
- 数据范围太大:尝试对输入数据进行归一化
python复制
x = (x - x.mean()) / x.std() - 随机初始化影响:设置随机种子保证可重复性
python复制torch.manual_seed(42)
8. 项目扩展与进阶方向
完成基础拟合后,可以考虑以下扩展:
- 更高维度的多项式拟合:尝试拟合y = x⁵ + 3x³ - 2x等更复杂的函数
- 多变量函数拟合:如z = x² + xy + y²
- 周期性函数拟合:尝试拟合sin(x)或cos(x)等函数
- 噪声数据拟合:在训练数据中添加高斯噪声,测试模型的鲁棒性
- 不同优化器比较:对比Adam、SGD、RMSprop等优化器的表现
在实际操作中,我发现几个关键点对结果影响很大:
- 数据范围的选择:对于这个三次函数,[-5,5]区间已经足够展示其非线性特性
- 网络深度的选择:太浅的网络难以拟合高阶非线性,太深的网络又容易过拟合
- 学习率的设置:需要多次尝试才能找到最佳值
最后一个小技巧:在训练初期可以先用较大的学习率快速下降,然后逐步减小学习率进行微调。这种策略在实践中往往能取得更好的效果。
