ML = 定义损失函数 + 用梯度下降最小化它。所有复杂性都藏在这两步里。

线性回归:三位一体

最小二乘法(MSE 损失)、最大似然估计(假设噪声服从正态分布)、正则方程(解析解 θ = (XᵀX)⁻¹Xᵀy)——这三种推导方式给出同一个答案。理解这个等价性,你就理解了为什么 MSE 是回归问题的自然选择。

package main

import "fmt"

// 一元线性回归:最小二乘法
// y = wx + b,最小化 Σ(yi - (w*xi + b))²
func linearRegression(xs, ys []float64) (w, b float64) {
    n := float64(len(xs))
    sumX, sumY, sumXY, sumXX := 0.0, 0.0, 0.0, 0.0
    for i, x := range xs {
        sumX += x
        sumY += ys[i]
        sumXY += x * ys[i]
        sumXX += x * x
    }
    // 正规方程解析解
    w = (n*sumXY - sumX*sumY) / (n*sumXX - sumX*sumX)
    b = (sumY - w*sumX) / n
    return
}

func main() {
    // 训练数据:y ≈ 2x + 1(加噪声)
    xs := []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    ys := []float64{2.9, 5.1, 6.8, 9.2, 11.0, 13.1, 15.0, 16.8, 18.9, 21.1}

    w, b := linearRegression(xs, ys)
    fmt.Printf("拟合结果: y = %.4fx + %.4f\n", w, b)  // ≈ y = 2x + 1

    // 预测
    predict := func(x float64) float64 { return w*x + b }
    fmt.Printf("x=11 预测: %.2f\n", predict(11))
}

Softmax 与数值稳定

Softmax 把 logits 转为概率分布:softmax(z)_i = e^{z_i} / Σ e^{z_j}。直接计算会溢出(e^1000 = inf)。数值稳定版本:先减去 max(z),数学上等价但避免溢出。这是实现细节,但体现了数学和工程的交叉——用等价变换保证数值稳定

package main

import (
    "fmt"
    "math"
)

// 数值不稳定版本(logits 大时溢出)
func softmaxNaive(z []float64) []float64 {
    out := make([]float64, len(z))
    sum := 0.0
    for _, zi := range z {
        sum += math.Exp(zi)
    }
    for i, zi := range z {
        out[i] = math.Exp(zi) / sum
    }
    return out
}

// 数值稳定版本:先减 max(数学等价)
func softmax(z []float64) []float64 {
    maxZ := z[0]
    for _, zi := range z { if zi > maxZ { maxZ = zi } }

    out := make([]float64, len(z))
    sum := 0.0
    for i, zi := range z {
        out[i] = math.Exp(zi - maxZ)
        sum += out[i]
    }
    for i := range out { out[i] /= sum }
    return out
}

func main() {
    // 正常 logits
    logits := []float64{2.0, 1.0, 0.1}
    fmt.Println("softmax:", softmax(logits))  // [0.659, 0.242, 0.099]

    // 大 logits:naive 溢出,stable 正常
    bigLogits := []float64{1000, 999, 998}
    fmt.Println("naive:", softmaxNaive(bigLogits))   // [NaN NaN NaN]
    fmt.Println("stable:", softmax(bigLogits))        // [0.665 0.245 0.090]

    // 交叉熵损失(用 log-softmax 合并计算,更稳定)
    logSoftmax := func(z []float64, i int) float64 {
        sm := softmax(z)
        return math.Log(sm[i])
    }
    // 真实标签是类别 0,预测 logits
    loss := -logSoftmax(logits, 0)
    fmt.Printf("交叉熵损失: %.4f\n", loss)
}

反向传播:全链路数学

一个两层神经网络的完整数学链路: 1. 前向传播z₁ = W₁x + b₁a₁ = ReLU(z₁)z₂ = W₂a₁ + b₂ŷ = softmax(z₂) 2. 损失L = CrossEntropy(y, ŷ) = -Σ yᵢ log ŷᵢ 3. 反向传播(链式法则):∂L/∂W₁ = ∂L/∂z₂ × ∂z₂/∂a₁ × ∂a₁/∂z₁ × ∂z₁/∂W₁ 4. 参数更新W₁ ← W₁ - η × ∂L/∂W₁

数学工具全景回顾

ML 操作背后的数学本课章节
数据表示向量、矩阵第 9-10 章
模型参数高维向量空间第 10 章
前向传播矩阵乘法 + 激活函数第 9 章
损失函数概率论 + 信息论(熵)第 8、18 章
梯度计算微积分(偏导数)第 11 章
反向传播链式法则第 11 章
参数更新梯度下降(优化)第 11 章
卷积神经网络傅里叶变换(卷积定理)第 13 章
Transformer 注意力点积(向量内积)第 10 章
模型评估统计假设检验第 8 章
口诀ML 不是黑魔法,每行代码背后都有章节可查。
package main

import (
    "fmt"
    "math"
    "math/rand"
)

// 最小神经网络:1 个隐层,ReLU,梯度下降
// 解决 XOR 问题(线性不可分,需要至少 1 个隐层)

func relu(x float64) float64 { return math.Max(0, x) }
func reluGrad(x float64) float64 { if x > 0 { return 1 }; return 0 }
func sigmoid(x float64) float64 { return 1 / (1 + math.Exp(-x)) }

func trainXOR() {
    // 权重初始化(小随机值)
    w1 := [2][2]float64{{rand.NormFloat64() * 0.5, rand.NormFloat64() * 0.5},
        {rand.NormFloat64() * 0.5, rand.NormFloat64() * 0.5}}
    b1 := [2]float64{0, 0}
    w2 := [2]float64{rand.NormFloat64() * 0.5, rand.NormFloat64() * 0.5}
    b2 := 0.0
    lr := 0.5

    // XOR 数据
    X := [][2]float64{{0, 0}, {0, 1}, {1, 0}, {1, 1}}
    Y := []float64{0, 1, 1, 0}

    for epoch := 0; epoch < 5000; epoch++ {
        totalLoss := 0.0
        for d, x := range X {
            // 前向传播
            z1 := [2]float64{w1[0][0]*x[0] + w1[0][1]*x[1] + b1[0],
                w1[1][0]*x[0] + w1[1][1]*x[1] + b1[1]}
            a1 := [2]float64{relu(z1[0]), relu(z1[1])}
            z2 := w2[0]*a1[0] + w2[1]*a1[1] + b2
            yhat := sigmoid(z2)
            y := Y[d]
            totalLoss += -y*math.Log(yhat+1e-8) - (1-y)*math.Log(1-yhat+1e-8)

            // 反向传播
            dL_dyhat := (yhat - y) / (yhat * (1 - yhat) + 1e-8)
            dyhat_dz2 := yhat * (1 - yhat)
            dz2 := dL_dyhat * dyhat_dz2

            w2[0] -= lr * dz2 * a1[0]
            w2[1] -= lr * dz2 * a1[1]
            b2 -= lr * dz2

            for i := 0; i < 2; i++ {
                da1 := dz2 * w2[i]
                dz1 := da1 * reluGrad(z1[i])
                w1[i][0] -= lr * dz1 * x[0]
                w1[i][1] -= lr * dz1 * x[1]
                b1[i] -= lr * dz1
            }
        }
        if epoch%1000 == 999 {
            fmt.Printf("Epoch %4d: loss=%.4f\n", epoch+1, totalLoss/4)
        }
    }

    // 验证
    fmt.Println("XOR 预测:")
    for _, x := range X {
        z1 := [2]float64{w1[0][0]*x[0]+w1[0][1]*x[1]+b1[0], w1[1][0]*x[0]+w1[1][1]*x[1]+b1[1]}
        a1 := [2]float64{relu(z1[0]), relu(z1[1])}
        z2 := w2[0]*a1[0] + w2[1]*a1[1] + b2
        fmt.Printf("  XOR(%v,%v) = %.3f\n", int(x[0]), int(x[1]), sigmoid(z2))
    }
}

func main() { trainXOR() }
推荐做法
  • 用 log-softmax + NLLLoss 代替 softmax + CrossEntropy——数值更稳定
  • 梯度检验:数值梯度和解析梯度之差 < 1e-5 才算正确
  • 理解每层输出的维度变化——维度错误是 bug 的最大来源
不推荐
  • 用 MSE 做分类损失——交叉熵收敛更快,梯度更好
  • 权重全初始化为 0——对称性导致所有神经元学到相同特征
常见误区
  • 学习率对不同参数层应该不同(Adam 的本质之一)——统一 lr 是近似

判断标准:能从零手写 XOR 神经网络的前向+反向传播,并看到它收敛 → 完成本课程。

数学是程序员的第二母语。你学会它的那一天,你的代码就有了灵魂。

— 本课结语