哈喽,大家好~
均方误差,即:Mean Squared Error, MSE。
今儿就来和大家聊聊MSE,具体来看看MSE 在干什么?
比如,你现在做回归问题:给输入 x,模型给出预测 ,真实值是 。MSE 做的一件非常直观的事,就是把每个预测错误( )看成一个“偏差”,把这个偏差平方,然后把所有样本的平方误差求平均。
MSE 想要让平均的“误差平方”尽可能小。
为什么平方?两点直观理由:
-
平方会放大大的错误。也就是对大偏差“更严格”,使模型尽量避免几次很大的错。 -
平方使得损失关于预测是连续且可微的(对大多数模型友好,便于梯度下降优化)。
一句话总结,就是MSE 把“我们希望预测尽量接近真实值”量化成了一个连续且可导的数值目标,优化这个目标就等于把预测向真实值拉近。
MSE 的数学表达
定义(样本级):
常见形式:
-
平均平方误差:上面直接使用。 -
有时候会使用总和平方误差 (Sum of Squared Errors, SSE): ,只差一个常数因子。
关于梯度(对单个预测 ):
则
这告诉我们:误差的方向和大小决定了梯度的方向与幅度。误差越大,梯度越大(学习步子越大),有助于迅速纠正大错误。
与概率解释(与高斯噪声等价):
如果我们假设观测噪声 服从零均值、同方差 的高斯分布:
最大似然估计(MLE)等价于最小化负对数似然,而负对数似然在高斯噪声下正比于平方误差。因此最小化 MSE 等价于对高斯噪声假设下的 MLE。
MSE 的优缺点
优点:
-
连续且可微,便于用梯度优化器训练神经网络。 -
对于高斯噪声的回归问题,MSE 有统计合理性(MLE)。 -
实现简单,解释直观。
缺点:
-
对异常值很敏感,一两个异常点会极大影响模型,因为误差被平方放大。 -
假设等方差,对于异方差数据(噪声随 x 变化)用 MSE 可能不理想。
替代方案:
-
MAE(平均绝对误差):对异常值更鲁棒,但不可导点在 0(数值方法解决通常没问题)。 -
Huber 损失:在小误差时像 MSE,在大误差时像 MAE,兼顾两者优点。 -
负对数似然(如果能建模异方差或非高斯噪声):更灵活,能学噪声的分布。
完整案例
我们用一个小神经网络拟合一个带有异方差噪声的非线性函数,用 MSELoss 训练,并生成多张用于诊断与分析的图。
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt
import seaborn as sns
# 1. 构造虚拟数据(异方差 + 非线性函数)
np.random.seed(42)
N = 1000
x = np.linspace(-3, 3, N)
# 真实函数:非线性(正弦 + 多项式)
y_true = 0.8 * np.sin(1.75 * x) + 0.3 * x**2 - 0.2 * x
# 异方差噪声:噪声随 x 的绝对值增长
noise_std = 0.2 + 0.5 * (np.abs(x) / max(np.abs(x)))
y = y_true + np.random.normal(0, noise_std)
# 转为 PyTorch 张量
X = torch.tensor(x, dtype=torch.float32).unsqueeze(1) # shape (N,1)
Y = torch.tensor(y, dtype=torch.float32).unsqueeze(1)
dataset = TensorDataset(X, Y)
loader = DataLoader(dataset, batch_size=32, shuffle=True)
# 2. 简单神经网络模型
class SmallNet(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, 1)
)
def forward(self, x):
return self.net(x)
model = SmallNet()
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# 3. 训练(记录训练过程)
epochs = 800
loss_history = []
grad_norm_history = []
for epoch in range(epochs):
model.train()
epoch_loss = 0.0
for xb, yb in loader:
pred = model(xb)
loss = criterion(pred, yb)
optimizer.zero_grad()
loss.backward()
# 记录梯度范数(诊断用)
total_norm = 0.0
for p in model.parameters():
if p.grad isnotNone:
param_norm = p.grad.data.norm(2)
total_norm += param_norm.item()**2
total_norm = total_norm**0.5
grad_norm_history.append(total_norm)
optimizer.step()
epoch_loss += loss.item() * xb.size(0)
epoch_loss /= N
loss_history.append(epoch_loss)
if epoch % 100 == 0:
print(f"Epoch {epoch}, loss {epoch_loss:.4f}")
# 4. 预测并计算残差
model.eval()
with torch.no_grad():
X_all = torch.tensor(np.linspace(-3, 3, 400), dtype=torch.float32).unsqueeze(1)
y_pred_all = model(X_all).squeeze().numpy()
y_pred_train = model(X).squeeze().numpy()
residuals = (Y.squeeze().numpy() - y_pred_train)
# 5. 可视化分析
plt.rcParams['figure.dpi'] = 120
# 图 1:数据 + 真实函数 + 初始模型预测(为了显示初始状态,这里重新初始化一个模型)
init_model = SmallNet()
with torch.no_grad():
init_pred = init_model(X_all).squeeze().numpy()
fig1, ax1 = plt.subplots(figsize=(8,5))
ax1.scatter(x, y, s=25, color='tab:blue', alpha=0.7, label='noisy samples')
ax1.plot(x, y_true, color='red', linewidth=2.0, label='true function')
ax1.plot(X_all.squeeze().numpy(), init_pred, color='tab:green', linestyle='--', linewidth=2, label='initial model pred')
ax1.set_title("Data and True Function vs Initial Random Prediction", fontsize=12)
ax1.legend()
plt.tight_layout()
plt.show()
# 图 2:训练后预测与真实函数(拟合效果)
fig2, ax2 = plt.subplots(figsize=(8,5))
ax2.scatter(x, y, s=20, color='lightcoral', alpha=0.6, label='noisy samples')
ax2.plot(x, y_true, color='purple', linewidth=2.0, label='true function')
ax2.plot(X_all.squeeze().numpy(), y_pred_all, color='orange', linewidth=2.5, label='trained model pred')
# 可视化预测误差带(using empirical residual std by bin)
# 计算局部残差标准差用于绘制粗略置信带
bins = np.linspace(-3, 3, 20)
inds = np.digitize(x, bins)
stds = []
centers = 0.5*(bins[:-1]+bins[1:])
for i in range(1, len(bins)):
mask = inds==i
if mask.sum()>0:
stds.append(np.std(y[mask] - np.interp(bins[i-1:i+1].mean(), X_all.squeeze().numpy(), y_pred_all)))
else:
stds.append(0)
ax2.fill_between(X_all.squeeze().numpy(), y_pred_all - 0.6, y_pred_all + 0.6, color='orange', alpha=0.15)
ax2.set_title("Trained Model Prediction vs True Function", fontsize=12)
ax2.legend()
plt.tight_layout()
plt.show()
# 图 3:训练损失曲线 & 梯度范数(子图)
fig3, (ax3a, ax3b) = plt.subplots(1,2, figsize=(12,4))
ax3a.plot(range(len(loss_history)), loss_history, color='tab:blue', linewidth=2)
ax3a.set_xlabel('Epoch')
ax3a.set_ylabel('MSE Loss')
ax3a.set_title('Training Loss Curve')
ax3b.plot(range(len(grad_norm_history)), grad_norm_history, color='tab:green', linewidth=1)
ax3b.set_xlabel('Step')
ax3b.set_ylabel('Grad Norm (L2)')
ax3b.set_title('Gradient Norm per Update')
plt.tight_layout()
plt.show()
# 图 4:残差分析(直方图 + 残差 vs 预测散点)
fig4, (ax4a, ax4b) = plt.subplots(1,2, figsize=(12,4))
sns.histplot(residuals, kde=True, color='tab:purple', ax=ax4a)
ax4a.set_title('Residuals Histogram (with KDE)')
ax4a.set_xlabel('Residual (y - y_pred)')
ax4a.axvline(0, color='black', linestyle='--')
# residual vs predicted
sc = ax4b.scatter(y_pred_train, residuals, c=np.abs(residuals), cmap='plasma', s=25, alpha=0.8)
ax4b.axhline(0, color='black', linestyle='--')
ax4b.set_xlabel('Predicted y')
ax4b.set_ylabel('Residual (y - y_pred)')
ax4b.set_title('Residuals vs Predicted (color by |residual|)')
plt.colorbar(sc, ax=ax4b, label='|residual|')
plt.tight_layout()
plt.show()
核心步骤
数据生成(异方差 + 非线性)
-
我用了一个混合函数: 作为真实函数。 -
噪声的标准差随 增大而增大: noise_std = 0.2 + 0.5 * (|x|/max(|x|))。这样生成的噪声是异方差的,能更真实地测试 MSE 在非理想情形下的表现。 -
目的:让你看到 MSE 在面对“不同位置噪声大小不一”的数据时会如何拟合(是否偏向数据密集或噪声小的区域)。
模型结构
-
小型 MLP:两层 64 单元、ReLU 激活,最后一个线性层输出标量。 -
选择很小的网络是为了示范,过大的网络会过拟合,影响可解释性。
损失函数与优化器
-
使用 nn.MSELoss()作为损失函数,optimizer = Adam(lr=1e-3)。 -
MSE 的梯度形式为 ,在实现上 PyTorch 会自动计算并反向传播梯度。
训练过程记录
-
记录每个 epoch 的平均损失 loss_history,便于画损失曲线。 -
同时记录每一步(每个 mini-batch)梯度范数 grad_norm_history,可用于检测梯度消失/爆炸或训练是否稳定。
预测与残差
-
使用训练好的模型对输入区间做密集预测(400 个点)画出预测曲线。 -
计算训练集上的残差 residuals = y - y_pred,用于残差分析图(残差分布、残差与预测值关系)。
可视化分析
Data and True Function vs Initial Random Prediction:
横轴 x,点是带噪声的观测样本(蓝色点),红色为真实函数曲线,绿色虚线为随机初始化模型的初始预测。
直观地展示数据、真实函数与未训练模型的初始差距。通常初始模型表现是随机的,起到基准参考。
-
如果初始预测和数据已经“很接近”(极少见),可能初始化偏置或数据很简单。 -
如果数据有明显的异方差(噪声随 x 变化),能从点云可视化看出(点在两侧更分散)。
Trained Model Prediction vs True Function:
训练后模型预测(橙色实线)与真实函数(紫色)、观测样本(浅珊瑚色)叠加。
检验模型整体拟合质量,是否能捕捉非线性趋势,以及在噪声大的区域是否过拟合或欠拟合。
Training Loss Curve & Gradient Norm per Update:
损失曲线,观察 MSE 是否持续下降与是否收敛。
梯度范数,观察训练中梯度的稳定性(是否爆炸或消失)。
Residuals Histogram + Residuals vs Predicted:
残差直方图 & KDE,查看残差分布是否近似对称、是否为零均值、是否接近高斯(MSE 假设下)。
残差 vs 预测值,查看残差是否随预测值呈系统性变化(异方差的一个迹象)。
另外:我在第 2 张图里还绘制了一个简单的“置信带”示意(fill_between),表示预测 +/- 一个固定带宽(示意性的),你可以进一步用残差在局部区间计算标准差,来绘出更精确的误差带(若需要概率解释,应建立带方差输出的模型)。
总结
MSE 的本质是把“预测误差”平方后求平均,使得损失对大错更敏感,并且便于梯度优化。
在假设高斯噪声和同方差的情况下,最小化 MSE 等价于最大化似然,是一个合理的选择。
MSE 对异常值敏感,对异方差数据也可能不是最优的选择;必要时应采用 Huber、MAE 或更复杂的概率模型。
在实验中,除了关注训练损失下降外,残差分析(残差分布、残差 vs 预测)是非常重要的诊断手段,它能揭示噪声结构、模型偏差与是否需要改进损失或模型。
最后

