导数 = 切线斜率 = 瞬时变化率 = 梯度的分量。这四个说法是同一件事。

数值导数:差分近似

导数的定义是极限 f'(x) = lim_{h→0} (f(x+h) - f(x)) / h。数值上用有限差分近似,中心差分 (f(x+h) - f(x-h)) / (2h) 比前向差分精度高一阶(误差 O(h²) vs O(h))。

package main

import (
    "fmt"
    "math"
)

// 数值导数(中心差分,精度 O(h²))
func derivative(f func(float64) float64, x, h float64) float64 {
    return (f(x+h) - f(x-h)) / (2 * h)
}

// 数值梯度(多元函数偏导数向量)
func gradient(f func([]float64) float64, x []float64, h float64) []float64 {
    grad := make([]float64, len(x))
    for i := range x {
        xi := x[i]
        x[i] = xi + h
        fPlus := f(x)
        x[i] = xi - h
        fMinus := f(x)
        x[i] = xi
        grad[i] = (fPlus - fMinus) / (2 * h)
    }
    return grad
}

func main() {
    // sin'(x) = cos(x)
    x := math.Pi / 4
    numDeriv := derivative(math.Sin, x, 1e-5)
    analytic := math.Cos(x)
    fmt.Printf("数值导数: %.8f\n", numDeriv)
    fmt.Printf("解析导数: %.8f\n", analytic)
    fmt.Printf("误差:     %.2e\n", math.Abs(numDeriv-analytic))

    // f(x,y) = x² + y² 在 (1,1) 处的梯度 = [2,2]
    f := func(v []float64) float64 { return v[0]*v[0] + v[1]*v[1] }
    grad := gradient(f, []float64{1, 1}, 1e-5)
    fmt.Printf("梯度: [%.4f, %.4f]\n", grad[0], grad[1])  // [2 2]
}

梯度下降:沿最陡下降方向走

package main

import (
    "fmt"
    "math"
)

// 梯度下降:最小化 f(x)
func gradientDescent(
    f func([]float64) float64,
    init []float64,
    lr float64,
    steps int,
) []float64 {
    x := make([]float64, len(init))
    copy(x, init)
    h := 1e-5

    for step := 0; step < steps; step++ {
        // 数值梯度
        grad := make([]float64, len(x))
        for i := range x {
            xi := x[i]
            x[i] = xi + h; fp := f(x)
            x[i] = xi - h; fm := f(x)
            x[i] = xi
            grad[i] = (fp - fm) / (2 * h)
        }
        // 参数更新:沿负梯度方向
        for i := range x {
            x[i] -= lr * grad[i]
        }
        if step%100 == 0 {
            fmt.Printf("step %4d: f=%.6f x=[%.4f,%.4f]\n", step, f(x), x[0], x[1])
        }
    }
    return x
}

func main() {
    // 最小化 f(x,y) = (x-3)² + (y+1)²,最小值在 (3,-1)
    f := func(v []float64) float64 {
        dx, dy := v[0]-3, v[1]+1
        return dx*dx + dy*dy
    }
    result := gradientDescent(f, []float64{0, 0}, 0.1, 500)
    fmt.Printf("最小值点: [%.4f, %.4f]\n", result[0], result[1])
    _ = math.Abs
}

链式法则与反向传播

神经网络是复合函数 L(f₃(f₂(f₁(x))))。求 L 对第一层参数的导数,用链式法则:∂L/∂w₁ = ∂L/∂f₃ × ∂f₃/∂f₂ × ∂f₂/∂f₁ × ∂f₁/∂w₁反向传播就是自动化地计算这些链式乘积,从输出层往输入层反向传递梯度。

package main

import (
    "fmt"
    "math"
)

// 简单计算图:z = sigmoid(wx + b)
// 前向传播 + 反向传播

func sigmoid(x float64) float64 {
    return 1 / (1 + math.Exp(-x))
}

func sigmoidGrad(x float64) float64 {
    s := sigmoid(x)
    return s * (1 - s)  // sigmoid 的导数
}

func main() {
    w, x, b := 2.0, 1.5, -1.0

    // 前向传播
    z := w*x + b         // z = 2
    a := sigmoid(z)      // a = sigmoid(2) ≈ 0.88
    loss := (a - 1) * (a - 1)  // L = (a-1)²

    // 反向传播(链式法则)
    dL_da := 2 * (a - 1)         // ∂L/∂a
    da_dz := sigmoidGrad(z)      // ∂a/∂z
    dz_dw := x                   // ∂z/∂w

    dL_dw := dL_da * da_dz * dz_dw  // 链式法则
    fmt.Printf("前向: z=%.4f a=%.4f loss=%.4f\n", z, a, loss)
    fmt.Printf("反向: ∂L/∂w = %.6f\n", dL_dw)
}
推荐做法
  • 调参之前先验证数值梯度 ≈ 解析梯度(梯度检验)
  • 学习率是最重要的超参数:太大发散,太小不收敛
  • 损失函数不下降时先检查梯度是否消失(接近 0)
不推荐
  • 在有解析梯度的情况下用数值梯度——计算开销大 O(n) 倍
  • 梯度下降迭代次数固定——改用 early stopping 或 loss 阈值
常见误区
  • 学习率对 loss 曲线的影响是非线性的——网格搜索而非线性调整

判断标准:能手写链式法则推导 ∂L/∂w,并解释负梯度方向 → 掌握本章。

微积分是人类第一次学会描述「正在发生」的数学,而不只是「已经发生」的结果。

— Steven Strogatz《微积分的力量》