神经网络如何更新参数:以线性拟合为例
在机器学习中,神经网络是一种强大的工具,能够通过学习数据中的模式来进行预测。在本篇博客中,我们将重点讨论神经网络如何更新参数(权重和偏置),并通过一个简单的线性拟合示例来演示这一过程。
1. 什么是线性拟合?
线性拟合是指通过一条直线来近似表达数据点之间的关系。简单来说,我们希望找到一个线性方程:
y = w x + b y = wx + b y=wx+b
其中, y y y 是输出, x x x 是输入特征, w w w 是权重, b b b 是偏置。我们的目标是通过训练数据来学习合适的 w w w 和 b b b。
2. 神经网络的基本结构
在本示例中,我们将使用一个简单的神经网络模型来进行线性拟合。模型结构如下:
- 输入层:接收输入特征 x x x。
- 输出层:生成预测值 y y y。
在 PyTorch 中,我们可以使用 nn.Linear
来创建一个线性层,该层会自动处理权重和偏置的初始化。
3. 损失函数
为了评估模型的预测效果,我们使用均方误差(Mean Squared Error, MSE)作为损失函数,其公式如下:
MSE = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 \text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 MSE=n1i=1∑n(yi−y^i)2
其中, y i y_i yi 是真实值, y ^ i \hat{y}_i y^i 是模型预测值, n n n 是样本数量。我们的目标是最小化这个损失函数。
4. 反向传播与权重更新
4.1 反向传播
反向传播是神经网络训练的核心算法。它通过链式法则计算损失函数相对于每个参数(权重和偏置)的梯度。具体步骤如下:
- 前向传播:计算模型的输出。
- 计算损失:通过损失函数评估输出与真实值之间的差异。
- 计算梯度:通过反向传播算法计算损失相对于每个参数的梯度。
4.2 权重更新
根据计算得到的梯度,我们可以使用梯度下降法更新权重和偏置。更新公式如下:
w
:
=
w
−
η
∂
MSE
∂
w
=
w
−
η
(
−
2
n
∑
i
=
1
n
(
y
i
−
(
w
⋅
x
i
+
b
)
)
⋅
x
i
)
w := w - \eta \frac{\partial \text{MSE}}{\partial w} = w - \eta \left( -\frac{2}{n} \sum_{i=1}^{n} (y_i - (w \cdot x_i + b)) \cdot x_i \right)
w:=w−η∂w∂MSE=w−η(−n2i=1∑n(yi−(w⋅xi+b))⋅xi)
b : = b − η ∂ MSE ∂ b = b − η ( − 2 n ∑ i = 1 n ( y i − ( w ⋅ x i + b ) ) ) b := b - \eta \frac{\partial \text{MSE}}{\partial b} = b - \eta \left( -\frac{2}{n} \sum_{i=1}^{n} (y_i - (w \cdot x_i + b)) \right) b:=b−η∂b∂MSE=b−η(−n2i=1∑n(yi−(w⋅xi+b)))
- 这里的 η \eta η 是学习率,控制每次更新的步长。
- 通过多次迭代更新,我们希望能找到最优的 w w w 和 b b b。
5. 示例代码
下面是使用 PyTorch 进行线性拟合的完整代码示例:
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import imageio.v2 as imageio # 使用 imageio.v2 导入
import os
# 设置随机种子
np.random.seed(42)
torch.manual_seed(42)
# 生成数据
X = np.linspace(-1, 1, 100).reshape(-1, 1) # 输入特征
y = 2 * X + 1 + np.random.normal(0, 0.1, X.shape) # 线性关系 y = 2x + 1 + 噪声
# 转换为 PyTorch 张量
X_tensor = torch.FloatTensor(X)
y_tensor = torch.FloatTensor(y)
# 可视化数据
plt.scatter(X, y, label='Data Points', color='blue')
plt.title("Synthetic Data for Linear Fitting")
plt.xlabel("X")
plt.ylabel("y")
plt.legend()
plt.show()
# 创建线性回归模型
model = nn.Sequential(
nn.Linear(1, 1) # 1个输入特征,1个输出
)
# 定义损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失
optimizer = optim.SGD(model.parameters(), lr=0.1) # 学习率为 0.1
# 训练模型并记录拟合过程
epochs = 50
images = [] # 用于存储每一步的图像
for epoch in range(epochs):
model.train() # 设置模型为训练模式
# 前向传播
optimizer.zero_grad() # 清空梯度
y_pred = model(X_tensor) # 预测
# 计算损失
loss = criterion(y_pred, y_tensor)
# 反向传播
loss.backward() # 计算梯度
# 打印梯度
print(f'Epoch {epoch + 1}/{epochs}, Loss: {loss.item():.4f}')
for name, param in model.named_parameters():
if param.grad is not None:
print(f'Gradient for {name}: {param.grad.data.numpy()}')
# 更新参数
optimizer.step() # 更新参数
# 绘制图像
plt.figure()
plt.scatter(X, y, label='Data Points', color='blue')
plt.plot(X, y_pred.detach().numpy(), label='Fitted Line', color='red')
plt.title(f"Epoch {epoch + 1}/{epochs}, Loss: {loss.item():.4f}")
plt.xlabel("X")
plt.ylabel("y")
plt.legend()
plt.xlim(-1.5, 1.5)
plt.ylim(-1.5, 3.5)
# 保存图像
plt.savefig(f'epoch_{epoch + 1}.png')
plt.close()
# 生成动图
for epoch in range(1, epochs + 1):
images.append(imageio.imread(f'epoch_{epoch}.png'))
imageio.mimsave('linear_fitting.gif', images, duration=0.5)
# 清理生成的图像文件
for epoch in range(1, epochs + 1):
os.remove(f'epoch_{epoch}.png')
print("GIF saved as 'linear_fitting.gif'")
6. 结果与分析
- 损失值:随着训练的进行,损失值应该逐渐降低,表明模型的预测效果在改善。
- 梯度:每个参数的梯度将显示在控制台中,反映出当前参数对损失的影响程度。
训练过程的结果
在训练过程中,我们记录了每个 epoch 的损失值和每个参数的梯度。以下是结果的总结:
Epoch | Loss | Gradient for Weight | Gradient for Bias |
---|---|---|---|
1 | 0.5585 | ([-0.8449708]) | ([-0.31921485]) |
2 | 0.4804 | ([-0.7875014]) | ([-0.25537187]) |
3 | 0.4146 | ([-0.7339407]) | ([-0.20429748]) |
4 | 0.3588 | ([-0.6840228]) | ([-0.163438]) |
5 | 0.3112 | ([-0.63750005]) | ([-0.13075046]) |
6 | 0.2704 | ([-0.5941415]) | ([-0.10460036]) |
7 | 0.2353 | ([-0.5537319]) | ([-0.08368032]) |
8 | 0.2051 | ([-0.5160706]) | ([-0.0669443]) |
9 | 0.1789 | ([-0.48097098]) | ([-0.05355542]) |
10 | 0.1563 | ([-0.4482584]) | ([-0.04284435]) |
11 | 0.1368 | ([-0.41777086]) | ([-0.03427548]) |
12 | 0.1198 | ([-0.3893568]) | ([-0.02742033]) |
13 | 0.1051 | ([-0.36287528]) | ([-0.02193622]) |
14 | 0.0923 | ([-0.3381949]) | ([-0.01754898]) |
15 | 0.0812 | ([-0.31519306]) | ([-0.01403922]) |
16 | 0.0716 | ([-0.2937557]) | ([-0.01123137]) |
17 | 0.0633 | ([-0.27377635]) | ([-0.0089851]) |
18 | 0.0560 | ([-0.25515595]) | ([-0.00718815]) |
19 | 0.0497 | ([-0.23780188]) | ([-0.00575048]) |
20 | 0.0443 | ([-0.22162813]) | ([-0.00460032]) |
21 | 0.0395 | ([-0.20655444]) | ([-0.00368026]) |
22 | 0.0354 | ([-0.19250602]) | ([-0.00294429]) |
23 | 0.0318 | ([-0.17941296]) | ([-0.00235539]) |
24 | 0.0287 | ([-0.16721046]) | ([-0.00188428]) |
25 | 0.0260 | ([-0.15583795]) | ([-0.00150746]) |
26 | 0.0237 | ([-0.14523886]) | ([-0.00120598]) |
27 | 0.0216 | ([-0.13536069]) | ([-0.00096482]) |
28 | 0.0198 | ([-0.12615432]) | ([-0.00077182]) |
29 | 0.0183 | ([-0.11757411]) | ([-0.00061745]) |
30 | 0.0170 | ([-0.10957752]) | ([-0.00049394]) |
31 | 0.0158 | ([-0.1021248]) | ([-0.00039512]) |
32 | 0.0148 | ([-0.09517898]) | ([-0.00031608]) |
33 | 0.0139 | ([-0.08870552]) | ([-0.00025291]) |
34 | 0.0132 | ([-0.08267231]) | ([-0.00020235]) |
35 | 0.0125 | ([-0.07704946]) | ([-0.00016194]) |
36 | 0.0119 | ([-0.07180902]) | ([-0.00012952]) |
37 | 0.0114 | ([-0.06692503]) | ([-0.00010365]) |
38 | 0.0110 | ([-0.06237321]) | ([-8.2912156e-05]) |
39 | 0.0106 | ([-0.05813103]) | ([-6.6339504e-05]) |
40 | 0.0103 | ([-0.05417732]) | ([-5.3104945e-05]) |
41 | 0.0100 | ([-0.05049255]) | ([-4.2504398e-05]) |
42 | 0.0098 | ([-0.04705839]) | ([-3.4027966e-05]) |
43 | 0.0096 | ([-0.04385783]) | ([-2.724817e-05]) |
44 | 0.0094 | ([-0.04087488]) | ([-2.1751272e-05]) |
45 | 0.0092 | ([-0.03809486]) | ([-1.7460436e-05]) |
46 | 0.0091 | ([-0.03550391]) | ([-1.399708e-05]) |
47 | 0.0090 | ([-0.03308916]) | ([-1.1263182e-05]) |
48 | 0.0088 | ([-0.03083867]) | ([-8.999137e-06]) |
49 | 0.0088 | ([-0.02874126]) | ([-7.2116964e-06]) |
50 | 0.0087 | ([-0.02678646]) | ([-5.795853e-06]) |
7. 什么是二阶拟合?
二阶拟合是指通过一条二次曲线来近似表达数据点之间的关系。简单来说,我们希望找到一个二次方程:
y = a x 2 + b x + c y = ax^2 + bx + c y=ax2+bx+c
其中, y y y 是输出, x x x 是输入特征, a a a、 b b b 和 c c c 是需要学习的参数。我们的目标是通过训练数据来学习合适的 a a a、 b b b 和 c c c。
8. 神经网络的基本结构
在本示例中,我们将使用一个简单的神经网络模型来进行二阶拟合。模型结构如下:
- 输入层:接收输入特征 x x x 和 x 2 x^2 x2。
- 隐藏层:使用一个隐藏层,包含多个神经元。
- 输出层:生成预测值 y y y。
9. 损失函数
为了评估模型的预测效果,我们使用均方误差(Mean Squared Error, MSE)作为损失函数,其公式如下:
MSE = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 \text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 MSE=n1i=1∑n(yi−y^i)2
其中, y i y_i yi 是真实值, y ^ i \hat{y}_i y^i 是模型预测值, n n n 是样本数量。我们的目标是最小化这个损失函数。
10. 反向传播与权重更新
10.1 反向传播
反向传播是神经网络训练的核心算法。它通过链式法则计算损失函数相对于每个参数(权重和偏置)的梯度。具体步骤如下:
- 前向传播:计算模型的输出。
- 计算损失:通过损失函数评估输出与真实值之间的差异。
- 计算梯度:通过反向传播算法计算损失相对于每个参数的梯度。
10.2 权重更新
根据计算得到的梯度,我们可以使用梯度下降法更新权重和偏置。更新公式如下:
w : = w − η ∂ MSE ∂ w w := w - \eta \frac{\partial \text{MSE}}{\partial w} w:=w−η∂w∂MSE
b : = b − η ∂ MSE ∂ b b := b - \eta \frac{\partial \text{MSE}}{\partial b} b:=b−η∂b∂MSE
其中, η \eta η 是学习率,控制每次更新的步长。通过多次迭代更新,我们希望能找到最优的 a a a、 b b b 和 c c c。
11. 示例代码
下面是使用 PyTorch 进行二阶拟合的完整代码示例:
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import imageio.v2 as imageio # 使用 imageio.v2 导入
import os
# 设置随机种子
np.random.seed(42)
torch.manual_seed(42)
# 生成数据
X = np.linspace(-1, 1, 100).reshape(-1, 1) # 输入特征
y = 2 * (X ** 2) + 1 + np.random.normal(0, 0.1, X.shape) # 二次关系 y = 2x^2 + 1 + 噪声
# 转换为 PyTorch 张量
X_tensor = torch.FloatTensor(X)
y_tensor = torch.FloatTensor(y)
# 可视化数据
plt.scatter(X, y, label='Data Points', color='blue')
plt.title("Synthetic Data for Quadratic Fitting")
plt.xlabel("X")
plt.ylabel("y")
plt.legend()
plt.show()
# 创建二次回归模型
class QuadraticModel(nn.Module):
def __init__(self):
super(QuadraticModel, self).__init__()
self.linear1 = nn.Linear(1, 10) # 隐藏层
self.linear2 = nn.Linear(10, 1) # 输出层
def forward(self, x):
x = torch.relu(self.linear1(x)) # 隐藏层激活
x = self.linear2(x) # 输出层
return x
# 实例化模型、损失函数和优化器
model = QuadraticModel()
criterion = nn.MSELoss() # 均方误差损失
optimizer = optim.SGD(model.parameters(), lr=0.1) # 学习率为 0.1
# 训练模型并记录拟合过程
epochs = 300
images = [] # 用于存储每一步的图像
for epoch in range(epochs):
model.train() # 设置模型为训练模式
# 前向传播
optimizer.zero_grad() # 清空梯度
y_pred = model(X_tensor) # 预测
# 计算损失
loss = criterion(y_pred, y_tensor)
# 反向传播
loss.backward() # 计算梯度
# 打印梯度
print(f'Epoch {epoch + 1}/{epochs}, Loss: {loss.item():.4f}')
for name, param in model.named_parameters():
if param.grad is not None:
print(f'Gradient for {name}: {param.grad.data.numpy()}')
# 更新参数
optimizer.step() # 更新参数
# 绘制图像
plt.figure()
plt.scatter(X, y, label='Data Points', color='blue')
plt.plot(X, y_pred.detach().numpy(), label='Fitted Curve', color='red')
plt.title(f"Epoch {epoch + 1}/{epochs}, Loss: {loss.item():.4f}")
plt.xlabel("X")
plt.ylabel("y")
plt.legend()
plt.xlim(-1.5, 1.5)
plt.ylim(-1.5, 3.5)
# 保存图像
plt.savefig(f'epoch_{epoch + 1}.png')
plt.close()
# 生成动图
for epoch in range(1, epochs + 1):
images.append(imageio.imread(f'epoch_{epoch}.png'))
imageio.mimsave('quadratic_fitting.gif', images, duration=0.5)
# 清理生成的图像文件
for epoch in range(1, epochs + 1):
os.remove(f'epoch_{epoch}.png')
print("GIF saved as 'quadratic_fitting.gif'")
12. 结果与分析
- 损失值:随着训练的进行,损失值应该逐渐降低,表明模型的预测效果在改善。
- 拟合曲线:每个 epoch 的拟合曲线将显示在生成的 GIF 动画中,帮助我们可视化模型的学习过程。