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 神经网络的前向+反向传播,并看到它收敛 → 完成本课程。
数学是程序员的第二母语。你学会它的那一天,你的代码就有了灵魂。
— 本课结语