当你看到实验数据中出现一个漂亮的"钟形曲线"时,很可能就遇到了统计学中最重要的分布——高斯分布(也叫正态分布)。这个看似简单的曲线背后,隐藏着自然界无数现象的统计规律。从物理实验测量误差到人群身高分布,从传感器噪声到金融数据波动,高斯分布无处不在。
高斯函数的标准形式看起来并不复杂:
f(x) = a * exp(-(x-μ)²/(2σ²))
其中a控制曲线高度,μ决定中心位置,σ影响曲线宽度。但当我们手头有一组实验数据想要拟合出这三个参数时,问题就变得有趣起来了。我在处理光谱仪数据时就经常遇到这个需求——每次测量得到的谱线都需要快速准确地拟合出峰位、峰强和峰宽。
直接拟合指数函数会遇到非线性优化的复杂问题。这里有个绝妙的技巧:对高斯函数两边取自然对数。这个操作就像变魔术一样,把棘手的指数函数变成了友好的二次函数:
ln(f(x)) = ln(a) - (x-μ)²/(2σ²) = Ax² + Bx + C
这个转换后,A、B、C与原始参数的关系一目了然:
A = -1/(2σ²)
B = μ/σ²
C = ln(a) - μ²/(2σ²)
假设我们有n个数据点(xi, yi),首先需要过滤掉yi≤0的点(因为对数运算需要正数)。然后计算以下累加量:
S_x4 = Σxi⁴
S_x3 = Σxi³
S_x2 = Σxi²
S_x = Σxi
S_1 = n
S_zx2 = Σ(ln(yi)*xi²)
S_zx2 = Σ(ln(yi)*xi)
S_z = Σln(yi)
这些累加量会构成一个3×3的线性方程组:
[S_x4 S_x3 S_x2][A] [S_zx2]
[S_x3 S_x2 S_x ][B] = [S_zx1]
[S_x2 S_x S_1 ][C] [S_z ]
解这个方程组有多种方法,我选择用克莱姆法则(Cramer's Rule)来实现,因为它直观且易于调试。核心是计算3×3矩阵的行列式:
python复制def det3(m):
return (m[0][0]*(m[1][1]*m[2][2]-m[1][2]*m[2][1])
-m[0][1]*(m[1][0]*m[2][2]-m[1][2]*m[2][0])
+m[0][2]*(m[1][0]*m[2][1]-m[1][1]*m[2][0]))
当行列式接近零时,说明数据点可能存在共线性问题,这时应该抛出异常提示用户检查数据质量。
解出A、B、C后,还原原始参数的公式展现了数学的对称美:
σ = √(-1/(2A))
μ = -B/(2A)
a = exp(C - B²/(4A))
这里有个重要检查点:A必须是负数,否则说明拟合过程可能出了问题。我在实际项目中就遇到过因数据质量差导致A为正数的情况,这时需要加入异常处理:
python复制if A_ >= 0:
raise ValueError("拟合失败:二次项系数应为负")
为了测试算法,我写了个数据生成器,其中加入了可控的随机噪声:
python复制def generate_data(a, mu, sigma, num_points=100):
x_vals = []; y_vals = []
for i in range(num_points):
x = -1 + i * 0.05
noise = random.uniform(-0.05, 0.05)
y = a * math.exp(-(x-mu)**2/(2*sigma**2)) + noise
y = max(y, 0.01) # 确保y为正数
x_vals.append(x); y_vals.append(y)
return x_vals, y_vals
特别注意两点:1) 噪声幅度不宜过大;2) y值要做非负处理。这些都是保证对数变换有效的前提。
python复制# 生成测试数据
x_data, y_data = generate_data(a=5.0, mu=2.0, sigma=1.5)
# 执行拟合
try:
a_fit, mu_fit, sigma_fit = gaussian_fit_log_least_squares(x_data, y_data)
print(f"拟合结果: a={a_fit:.4f}, μ={mu_fit:.4f}, σ={sigma_fit:.4f}")
except ValueError as e:
print("拟合失败:", str(e))
典型输出结果可能如下:
拟合结果: a=5.0123, μ=2.0009, σ=1.4988
可以看到,即使加入了5%的随机噪声,拟合结果依然非常接近真实参数值。
虽然这个对数变换法简单有效,但在实际项目中我发现几个需要注意的问题:
首先,它对数据中的离群点非常敏感。一个异常的y值就可能导致整个拟合失败。解决方法是在预处理阶段加入异常值过滤,或者使用更鲁棒的拟合方法(如RANSAC)。
其次,当数据存在背景噪声(如非零基线)时,这个方法会失效。这时可以考虑修改模型函数,增加背景项:
f(x) = a*exp(-(x-μ)²/(2σ²)) + b
最后,对于多峰拟合场景,这个方法需要配合峰值检测算法先进行数据分割。我在处理X射线衍射数据时就经常需要这种组合策略。