Skip to content

第2章 · Python 与数学最小坡道

本章是入门坡道,不属于 clean-room 合同,练习判分只做入口检查,不代表掌握。 先修:会基本电脑操作,会写 JavaScript(ES6:const/let、箭头函数、数组方法、class)。不需要任何 Python 或大学数学基础。

第3章一上来就要求写 Value.__add__、闭包、跑 pytest、读 NumPy shape。本章把这几样入口技能补齐:只教第3–10章直接消费的最小集,教到够用就停。

本章目标

  • 能写第3章需要的 Python:f-string、列表与字典、列表推导式、def 与默认参数、类的 __init__/__add__/__mul__/__call__、闭包、try/exceptwith
  • 能建虚拟环境、pip install、跑 pytest -q 并读懂断言失败输出。
  • 能用 NumPy 建数组、读 .shape、索引、broadcasting、用 @ 做矩阵乘。
  • 能手算一个 2×2 矩阵乘、用中心差商近似导数、手算偏导数和梯度向量、计算条件概率和贝叶斯更新、从抛硬币数据做 MLE。
  • 理解 Bernoulli / Categorical / Gaussian 三个分布的含义和在 LLM 中的位置。

本章不教什么

教多了就是跑偏。以下主题第3–10章不直接消费,本章一律不碰:

  • Python:装饰器、生成器、异步(async/await)、类型标注进阶、面向对象进阶(继承/多态)、包发布。
  • 数学:SVD、极限的严格定义、任何证明。

学的时候遇到这些概念,记下名字,跳过,继续走主线。

公式与 shape

中心差商(第3章的有限差分直接用它):

f(x)f(x+h)f(xh)2h,h105.

矩阵乘的 shape 规则(第6章起反复使用):

(M,K) @ (K,N)(M,N),out[i][j]=ka[i][k]b[k][j].
对象shape约束
向量(N,)有序的数字列表,顺序有意义
矩阵乘输入(M, K)(K, N)中间维 K 必须相等,否则报 shape 错
矩阵乘输出(M, N)每个元素是一行对一列的加权求和
行均值(broadcasting)(N, 1)减回 (N, M) 时必须 keepdims=True
差商步长 h标量太小(如 1e-12)只剩浮点噪声

环境与工具

你在 JS 里已经会"项目级依赖隔离 + 跑测试";Python 的对应物是 venv + pip + pytest

bash
# 建虚拟环境(≈ 项目级 node_modules,但是 Python 解释器级别的隔离)
python3 -m venv .venv
source .venv/bin/activate        # Windows 用 .venv\Scripts\activate

# 装依赖(≈ npm install)
python -m pip install numpy pytest

# 跑测试(≈ npm test;本仓库各章共用这一个跑法)
PYTHONPATH=python python -m pytest python/tests/test_w0p_python_math.py -q

pytest 的断言就是裸 assert:断言失败时 pytest 会展开左右两边的值给你看,不需要学断言库。

常见错误:

  • 包装上系统 Python 而不是 venv:pip install 之前先 source .venv/bin/activate,用 which python 确认指向 .venv
  • 在仓库根目录跑测试忘了 PYTHONPATH=pythonimport labsModuleNotFoundError

Python 最小集

每一节都是同一个节奏:你在 JS 里已经会 X,Python 的对应物是 Y,差异是 Z。配套练习在 python/labs/w0p_exercises.py,写完一节跑一节判分。

变量、数值与 f-string

js
// JS
const name = "ada";
const lr = 0.01;
console.log(`hello, ${name}! lr=${lr}`);
python
# Python
name = "ada"        # 没有 const/let,直接赋值;变量是名字贴到值上
lr = 0.01
print(f"hello, {name}! lr={lr}")   # f-string ≈ 模板字符串

差异:Python 没有分号习惯;None 对应 null;整数除法用 /// 永远是 float 除法(JS 只有后者)。

练习:format_greeting。常见错误:写了 f 前缀但用了单引号包双引号导致字符串截断;忘记 f 前缀时 {name} 原样输出不报错——这是最易漏的 bug。

列表、字典与列表推导式

js
// JS
const evens = [1, 2, 3, 4].filter(n => n % 2 === 0).map(n => n * n);
const counts = { a: 2, b: 1 };
python
# Python
evens = [n * n for n in [1, 2, 3, 4] if n % 2 == 0]   # 列表推导式 = filter+map 一步
counts = {"a": 2, "b": 1}                              # dict ≈ 对象字面量 / Map
counts.get("c", 0)                                     # ≈ counts["c"] ?? 0

差异:列表推导式是 [表达式 for 变量 in 序列 if 条件],表达式在最前面;第3章的 topo 收集、参数遍历都会用到它。

练习:squares_of_evenscount_words。常见错误:

  • 把列表当值传递:b = a 之后 b.append(...) 会改到 a(JS 数组同理,但 Python 函数参数也一样是引用,第3章改参数列表时会咬人)。
  • 缩进错:Python 用缩进表示代码块,混用 Tab 和空格直接报 IndentationError;统一 4 空格。

函数 def 与默认参数

python
def difference_quotient(f, x, h=1e-5):   # h 是默认参数,≈ JS 的 h = 1e-5
    return (f(x + h) - f(x - h)) / (2 * h)

差异:和 JS 几乎一样,但有一个著名陷阱——可变默认参数def f(seen=[])[] 只创建一次,多次调用之间共享。永远写 def f(seen=None) 再在函数体里初始化。

练习:safe_dividedifference_quotient。常见错误:上面这条;以及函数定义后忘了调用要加括号(JS 同,但 Python 里把函数对象当值传给 map/排序很常见,两个用途都合法,注意区分)。

类与 dunder 方法(第3章 Value 的直接前置)

js
// JS:运算符不能重载,只能写方法
class Scalar {
  constructor(data) { this.data = data; }
  add(other) { return new Scalar(this.data + other.data); }
}
python
# Python:dunder(double underscore)方法让运算符可用
class Scalar:
    def __init__(self, data):      # ≈ constructor
        self.data = data           # ≈ this.data,且 self 必须显式写出
    def __add__(self, other):      # 让 a + b 生效
        return Scalar(self.data + other.data)
    def __mul__(self, other):      # 让 a * b 生效
        return Scalar(self.data * other.data)

__call__ 让实例能像函数一样被调用:Linear(2, 1)(3) == 7。第3章的 Value 就是在这个骨架上加 .grad_backward;再往后 nn.Moduleforward 也是 __call__ 模式。

练习:ScalarLinear。常见错误:

  • __add__ 里写 self.data += other.data:这会改写原对象,组合运算结果漂移。dunder 要返回新对象。
  • 方法第一个参数忘写 self,报 TypeError: takes 1 positional argument but 2 were given

闭包(第3章 _backward 的直接前置)

你在 JS 里天天写闭包;Python 概念完全一样:内层函数记住定义处的外层变量,外层函数返回后变量仍然活着。

python
def make_multiplier(factor):
    def multiply(x):        # multiply 记住了 factor
        return factor * x
    return multiply

triple = make_multiplier(3)
triple(5)                   # 15:make_multiplier 已返回,factor 还在

第3章里每个算子创建时定义一个 _backward 闭包,记住当时的输入节点;反向传播时(创建之后很久)再调用它,靠的就是这个"记住"。

内层函数要修改外层变量时需要 nonlocal(只读不用):

python
def make_counter():
    n = 0
    def count():
        nonlocal n          # 没有这行,n += 1 会报 UnboundLocalError
        n += 1
        return n
    return count

练习:make_multipliermake_counter。常见错误:循环里定义闭包的晚绑定——所有闭包记住的是循环变量的最终值,用工厂函数或默认参数把当前值固定下来(JS 的 var 循环同款坑,let 没有,Python 的循环变量像 var)。

异常 try/exceptwith

python
try:
    y = a / b
except ZeroDivisionError:     # except 要写明捕哪类,别裸 except
    y = None

with open(path, encoding="utf-8") as f:   # ≈ try/finally 语法糖,保证文件关闭
    first = f.readline()
# 出了这个缩进块,文件已关闭

差异:try/except 对应 try/catchraise 对应 throwwith 在 JS 里没有对应物,课程后面读语料、读 checkpoint 都用它。

练习:read_first_linematmul_shape(中间维不等时 raise ValueError)。常见错误:裸 except:KeyboardInterrupt、拼写错误的 NameError 也吞掉,故障信号全丢。

NumPy:数组、shape、索引、broadcasting、@

python
import numpy as np

x = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
x.shape          # (2, 3):先读 shape 再动手,这是全课程的第一反射
x[:, 0]          # 第一列,shape (2,);≈ JS 的 x.map(row => row[0]) 但快得多
x.mean(axis=1)   # 每行均值,shape (2,)
x.mean(axis=1, keepdims=True)   # shape (2, 1)
x - x.mean(axis=1, keepdims=True)   # broadcasting:(2,3) 减 (2,1),逐行减均值

a = np.ones((2, 3)); b = np.ones((3, 4))
(a @ b).shape    # (2, 4):矩阵乘用 @,中间维必须相等

broadcasting 规则一句话:从最后一维往前对齐,维度相等或其中一方为 1 才能广播,否则报 operands could not be broadcast together

练习:first_columnnormalize_rowsmatmulmatmul_shape。常见错误:(N, M)(N,)——你以为在按行减,实际 shape 对不上直接报错(或更糟,维度恰好兼容时静默算错);要 keepdims=True 拿到 (N, 1)

数学最小集

矩阵乘 = 每一行对每一列做加权求和

手算这一个 2×2,算对就够:

[1234]@[5678]=[15+2716+2835+4736+48]=[19224350]

out[i][j] = 左矩阵第 i 行与右矩阵第 j 列对应元素相乘再求和。shape 规则 (M,K)@(K,N)→(M,N):中间维 K 必须相等,输出取左边的行数、右边的列数。注意力里的 Q@K^T、FFN 里的两次投影,全是这一条。

练习:manual_matmul_2x2(纯 Python 列表,不许用 NumPy),再用 matmul 对拍。

导数与差商

导数 = 函数在某点的瞬时变化率。不记得定义没关系,数值上能算就够:

f(x)f(x+h)f(xh)2h

例:f(x)=x³x=2 处,真值是 12;取 h=1e-5 算出来的差商误差在 1e-4 以内。第3章的 gradcheck 就是用这个公式对拍你手写的反向传播——两边对不上,就是你的 _backward 闭包写错了。h 不能取太小:1e-12 量级时分子只剩浮点噪声。

练习:difference_quotient

对数、指数与期望的直觉

只需要三条直觉,第4章直接消费:

  1. log 把乘法变加法log(a·b) = log a + log b。一串概率连乘会小到浮点下溢,取 log 后变成连加,数值稳定。
  2. 概率越小,-log(p) 越大p=1 时为 0,p=0.1 时约 2.30,p=0.01 时约 4.61。模型给正确 token 的概率越低,惩罚越重——这就是交叉熵损失的来源。
  3. 期望 = 按概率加权的平均E[x] = Σ pᵢ·xᵢ,就是你算过的加权平均,权重是概率、加起来等于 1。

练习:neg_log_likelihood。常见错误:概率越小的词 -log(p) 越大,惩罚越重;p=1 时为 0,p=0.1 时约 2.30,p=0.01 时约 4.61。

偏导数:对多变量函数"固定其他变量求变化"

单变量导数你已经熟悉:f(x)fx 处的瞬时变化率。现实中的函数往往依赖多个输入——房价取决于面积和地段,loss 取决于所有权重。偏导数就是对其中一个变量求导、把其他变量当常数:

fx=limh0f(x+h,y)f(x,y)h,fy=limh0f(x,y+h)f(x,y)h.

和单变量导数的区别只有一个:多了一个变量名。求 f/xy 被视为常数,求 f/y 时反过来。

例:f(x,y)=x2+3xy+y2,在 (1,2) 处:

  • f/x=2x+3y=2(1)+3(2)=8
  • f/y=3x+2y=3(1)+2(2)=7

第3章的 Value.backward() 本质上就是对每个权重求偏导数,然后把它们串起来。

几何直觉:二元函数 z=f(x,y) 的图像是一座山。在固定 y 的切面上,斜率就是 f/x;固定 x 的切面上,斜率是 f/y。两个斜率拼在一起就是该点的切线平面

梯度向量 ∇f

把两个偏导数打包成一个向量,就是梯度

f=[fxfy].

梯度的方向是函数增长最快的方向,反方向就是下降最快的方向。第6章的梯度下降就是沿着 L 一步步走的。

前端类比:CSS 的 linear-gradient(90deg, red, blue)90deg 就是梯度方向——从纯色(低变化)到最颜色变化最快的方向。想象你在颜色山上,梯度告诉你往哪个方向走颜色变化最剧烈。

链式法则(多变量版)

第3章的反向传播就是多变量链式法则的系统化执行。对 z=f(g(x,y),h(x,y))

zx=zggx+zhhx.

关键变化:共享节点的梯度要累加,不能覆盖。第3章的 a + a(同一个节点被用两次)就是这条规则的试金石。

前端类比深化:Vue 的 computed 属性 A 依赖 B 和 C,B 变了要重算 A,C 变了也要重算 A——两次"变化信号"到达 A 时是独立的,互不覆盖,合起来才是 A 的真正变化。

练习:partial_derivativegradient_vectorchain_rule_2layer。常见错误:对 x 求导时忘了把 y 当常数;链式法则只算了一条路径漏掉了共享节点。

向量 = 有序的数字列表(深化)

向量就是 [0.1, -0.7, 1.3] 这样一排有顺序的数字。顺序有意义:交换两个分量就是另一个向量。两个向量可以做三种基本运算:

  1. 数乘cv=[cv1,cv2,]——把向量拉伸 c
  2. 加法u+v=[u1+v1,u2+v2,]——首尾相接平移
  3. 点积(内积)ab=iaibi——一个数字,衡量"方向有多一致"

第4章的 embedding 表里,每一行就是一个词对应的向量。

点积:衡量两个向量的"默契度"

ab=|a||b|cosθ

|a|=ai2 是向量的模长(长度)。θ 是两个向量的夹角:

  • θ=0°(同向)→ cosθ=1 → 点积最大(完全一致)
  • θ=90°(垂直)→ cosθ=0 → 点积为 0(正交,毫无关系)
  • θ=180°(反向)→ cosθ=1 → 点积最小(完全相反)

Attention 的核心 Q·K 就是点积:query 和 key 越对齐,点积越大,attention weight 越高。

前端类比:两个 CSS 颜色向量 [r, g, b] 的点积衡量颜色相似度。纯白 [255,255,255] 和纯白点积最大,纯白和纯黑 [0,0,0] 点积为 0(正交),纯白和纯红 [255,0,0] 在中间。

练习:dot_product。常见错误:忘了两个向量长度必须相等;索引越界。

矩阵乘法回顾与深化

前面已有 2×2 手算,这里补两个关键性质:

转置:把矩阵的行和列互换,Aij=Aji

[123456]=[142536]

转置和矩阵乘的关系:(AB)=BA。先乘再转 = 先转每个因子再反向相乘。第6章推导 attention 的 Q@K^T 时会直接用到。

非方阵乘法:现实中的矩阵几乎都不是方阵。

  • Embedding 查询:(B,T)@(V,D)=(B,T,D)——把 token id 变成向量
  • Attention score:(B,H,T,d)@(B,H,d,T)=(B,H,T,T)——每个位置看每个位置
  • FFN:(B,T,D)@(D,4D)=(B,T,4D)——升维再降维

共同规律:(M,K)@(K,N)(M,N),中间维 K 必须对齐。

练习:matmul_2dtranspose。常见错误:内层循环的索引顺序反了导致结果转置;方阵假设导致非方阵报错。

特征值与特征向量(直觉级)

矩阵 A 乘向量 v,通常既改变方向又改变长度:

Av=λv.

如果结果只是把 v 拉伸(或压缩)了 λ 倍、方向不变,那 v 就是特征向量λ特征值

直觉比喻:你站在一张弹性网上,往某个方向推——网主要沿着这个方向拉伸或收缩。那些"主要拉伸方向"就是特征向量,拉伸倍数就是特征值。

  • λ>1:该方向被拉伸
  • 0<λ<1:该方向被压缩
  • λ<0:方向反转

第9章讨论 KV Cache 压缩和低秩分解时会遇到这个概念。本章不要求你手算特征值,能理解"矩阵变换中的主方向"就够了。

练习:本节无独立练习(特征值属选读)。matmul_2dtranspose 已经覆盖了核心操作。

条件概率:在已知条件下重新估算

你已经学过 P(A) 表示事件 A 发生的概率。条件概率问的是:如果已经知道 B 发生了,A 的概率变成多少?

P(A|B)=P(AB)P(B).

直觉:把分母从"所有可能"缩小到"B 发生的那些"。就像把世界重新定义为"只有 B 是真的"。

例:掷一个公平骰子,P(偶数)=3/6=0.5。如果已知点数 >2(B),那 P(偶数|B)=2/4=0.5

独立事件

如果 A 的发生不影响 B,反之亦然,称两者独立

P(AB)=P(A)P(B).

独立事件的链式法则最简:把每个概率直接乘起来。第4章的 naive Bayes 语言模型假设词与词之间独立(简化假设,现实中不成立但效果好)。

贝叶斯定理:看到数据后更新信念

贝叶斯定理是"先有猜测、再看证据、修正猜测"的数学表达:

P(H|E)=P(E|H)P(H)P(E).
  • P(H)先验——看数据前的信念(历史概率)
  • P(E|H)似然——如果假设 H 成立,看到证据 E 的概率
  • P(H|E)后验——看完证据后的更新信念

前端类比:A/B 测试里,你先认为版本 A 和 B 各有 50% 可能更好(先验);收集到 1000 个用户的点击数据后(证据),更新为"B 有 80% 概率更好"(后验)。第11章 RAG 检索也是这个思路——每个文档都有一个"相关"概率,检索结果更新这个概率。

常见分布:自然界的数据模板

概率分布描述"随机变量取各个值的概率长什么样"。

Bernoulli(伯努利):抛一次硬币,结果 0 或 1。

P(X=k)={pk=11pk=0

第4章预测下一个词是否为某个特定词,就是 Bernoulli 判断。

Categorical(分类/多项式):掷一个 K 面的骰子,结果 {0,1,,K1}

P(X=k)=pk,k=0K1pk=1.

第4章的 softmax 输出就是一个 categorical 分布——每个词都有一个概率,加起来为 1。temperature 参数直接调节这个分布的"尖锐度"。

Gaussian(正态/高斯):钟形曲线,实数轴上最常见的分布。

f(x)=12πσe(xμ)22σ2
  • μ:中心(均值)
  • σ:宽度(标准差)

第6章的权重初始化常用 Xavier/Gaussian,第10章的 KL 散度约束模型输出不要偏离先验太远。

MLE 最大似然估计:什么参数最"合理"?

给定一组观察数据,MLE 问:哪个参数值让这些数据最可能发生?

例:抛硬币 10 次,出现 7 次正面。最可能的真实正面概率 p 是多少?

似然函数:L(p)=p7(1p)3。对 p 求导并令其为 0,得到 MLE p^=7/10=0.7

更一般地,如果数据是 n 次独立试验中 k 次成功,MLE 估计 p^=k/n

第4章的关键桥梁:语言模型的交叉熵 loss yilogy^i 等价于负对数似然(NLL)。最小化 CE loss = 最大化数据的似然 = MLE。第4章的"训练就是让模型赋予正确词最高概率"这句话,数学上就是 MLE。

前端类比:MLE 就像从用户行为数据里反推"用户最可能的偏好参数是什么"——你看到 70% 的点击在左侧按钮,就推断用户偏好左侧布局的概率是 0.7。

练习:conditional_probabilitybayes_theorembernoulli_pmfcategorical_samplemle_bernoulli。常见错误:贝叶斯公式里把 P(H|E)P(E|H) 搞反;MLE 分母忘了是总试验次数。

自信息:越不可能的事,信息量越大

如果一个事件发生的概率是 P(x),它带来的自信息是:

I(x)=log2P(x).
  • P(x)=1(必然事件)→ I(x)=0 比特(没有信息量)
  • P(x)=0.5I(x)=1 比特
  • P(x)=0.125I(x)=3 比特

直觉:越罕见的事件,发生时带来越多的" surprise"。第4章的 -log(p) 损失就是这个原理——模型给低概率词高惩罚。

前端类比:console.log("bug fixed") 不意外(信息量小),console.log("宇宙崩溃") 极意外(信息量大)。

熵:不确定性的度量

一个随机变量 X是所有可能结果的自信息期望:

H(X)=xP(x)log2P(x).
  • 均匀分布(所有结果等可能)→ 熵最大(最不确定)
  • 确定分布(只有一个结果概率为 1)→ 熵为 0(完全确定)

例:公平硬币 P(H)=P(T)=0.5H=0.5log20.50.5log20.5=1 bit。每抛一次硬币最多得到 1 bit 信息。

第4章用熵衡量语言模型输出的"不确定性";perplexity 就是 2H

练习:entropy。常见错误:忘记 P(x)=00log0=0(极限定义)。

KL 散度:两个分布有多不同

KL 散度(Kullback-Leibler divergence)衡量分布 P 相对于 Q 的"信息损失":

DKL(PQ)=xP(x)log2P(x)Q(x).
  • DKL=0:P 和 Q 完全相同
  • DKL>0:P 比 Q 更" surprised"
  • 不对称DKL(PQ)DKL(QP)

第10章的 DPO 训练就是最小化模型输出分布与参考分布之间的 KL 散度。

前端类比:两个组件样式(CSS 变量集合)的差异度量——值越不同,KL 散度越大。

交叉熵:预测 vs 真实的"信息差"

交叉熵 combines 熵和 KL 散度:

H(P,Q)=xP(x)log2Q(x)=H(P)+DKL(PQ).
  • P:真实标签分布(one-hot:正确词概率为 1,其余为 0)
  • Q:模型预测分布(softmax 输出)
  • 训练目标:让 Q 逼近 P,使交叉熵最小

第4章的 cross-entropy loss 就是 H(P,Q)。当 P 是 one-hot 时,H(P)=0(标签分布完全确定),所以最小化 CE = 最小化 DKL(PQ) = 让模型输出匹配标签。

前端类比:你写了一个组件,期望行为是 P,实际行为是 Q——cross-entropy 衡量"期望"和"实际"之间的信息差距。

练习:self_informationentropycross_entropykl_divergence

Perplexity:模型每步需要多少"猜测"

Perplexity=2H(P).

可以理解为"模型每预测一个词平均需要试多少次才能猜对"。

  • PP = 1:模型完美预测(每次一次命中)
  • PP = 100:模型平均要在 100 个词里猜一个

第4章的语言模型评测直接用 perplexity——越低越好。它和 cross-entropy 是单调关系:PP=2CE,所以最小化 CE 等价于最小化 PP。

前端类比:你写 UI 组件时平均要试几次才能让按钮出现在正确位置——perplexity 越低说明你对布局越有把握。

损失曲面 = 地形图

想象你站在一座山上,目标是走到最低点(山谷)。这座山的形状就是损失曲面——每个点的坐标是 (θ1,θ2,,L(θ)),其中 θ 是模型参数,L 是损失值。

  • 平原:损失曲面平坦,梯度接近 0,参数更新很小
  • 陡坡:梯度很大,参数更新大步跳跃
  • 峡谷:一个方向陡、另一个方向缓——SGD 容易在陡的方向震荡
  • 局部低谷:不是全局最低,但比周围低——SGD 可能卡在这里

第6章的 loss landscape 可视化就是把这个高维曲面压到 2D 让你看到。

梯度下降:蒙眼下山

梯度下降是最朴素的优化算法:站在当前位置,往梯度反方向走一步:

θθηL(θ).
  • θ:当前位置(参数值)
  • L:梯度(山最陡的上坡方向)
  • L:最陡的下坡方向
  • η学习率——每步迈多大

前端类比

  • 蒙眼下山 = 梯度下降:你只能感觉到脚下地面的倾斜方向,不能看全貌
  • 步长 η = 每步迈多大:太大容易越过最低点(震荡),太小走得太慢
  • 峡谷地形 = SGD 的困境:陡的方向 Update 很大,缓的方向 Update 很小——需要自适应步长

学习率的影响

  • η 太大:在最低点两侧来回跳跃,甚至 diverges(损失爆炸)
  • η 太小:收敛极慢,可能卡在局部低谷
  • 实际训练:用 learning rate schedule——训练初期大步快走,后期小步微调

动量:带惯性的下山

纯 SGD 在峡谷里容易"之"字形震荡。动量(Momentum)给参数更新加一个惯性项:

vβvηL(θ),θθ+v.
  • v:速度(惯性 accumulated 历史梯度方向)
  • β:摩擦系数(通常 0.9),控制惯性保留多少

直觉:球从山上滚下来,遇到小坑不会卡住——惯性带着它冲过去。第6章的 SGD+Momentum 就是这个思想。

前端类比:滚动惯性 = iOS 列表滚动的"橡皮筋"效果——手指离开后,滚动不会立刻停止,而是带着惯性继续滑一段。

练习:gradient_descent_stepmomentum_update。常见错误:学习率太大导致 NaN;动量方向符号搞反(梯度是上坡,更新应该是下坡)。

感知器:神经网络的原子单元

感知器(Perceptron)是最简单的神经网络单元:

y=f(w1x1+w2x2+b).
  • w1,w2:权重(每个输入的重要性)
  • b:偏置(阈值偏移)
  • f:激活函数(引入非线性)

如果 f 是阶跃函数(step),感知器就是一个线性分类器——它画一条直线把两类分开。但单层感知器解决不了 XOR 问题(线性不可分)。

MLP:多层感知器

把多个感知器层叠起来,就得到 MLP(Multi-Layer Perceptron):

输入层 → 隐藏层₁ → 隐藏层₂ → … → 输出层

每一层都是 y=f(Wx+b),其中 f 通常是 ReLU:

ReLU(x)=max(0,x).

为什么需要非线性激活? 如果所有层都是线性(f(x)=x),那么多层线性变换 = 一层线性变换(W2(W1x)=(W2W1)x)。非线性激活打破了这种"坍缩",让深层网络能学习复杂的函数。

前端类比:

  • MLP = React 组件树:每一层接收 props(输入),经过处理(权重变换 + 激活),传给子组件
  • ReLU = CSS 的 clip / overflow: hidden:负值被裁掉,只让正值通过
  • 权重矩阵 = 组件的样式映射表:输入空间的每个维度对应一个".style"权重

训练循环骨架

第8章的 TinyGPT 训练循环就是这四个步骤的循环:

for epoch in epochs:
    logits = model(x)          # ① 前向传播
    loss = criterion(logits, y) # ② 计算损失(CE)
    loss.backward()            # ③ 反向传播(链式法则)
    optimizer.step()           # ④ 更新参数(梯度下降)
    optimizer.zero_grad()      # ⑤ 清零梯度(否则累加)

把第3章的 Value.backward()、第6章的优化器、第8章的 nn.Module 串起来,就是这个循环。

练习:relumlp_layer_forward。常见错误:忘了 ReLU 把负值截成 0;矩阵乘法 shape 不匹配。

从零实践

  1. 按「环境与工具」建好 venv、装好 numpy 和 pytest。

  2. 打开 python/labs/w0p_exercises.py,按上面各节的顺序把 raise NotImplementedError 逐个换成你的实现;不要改函数名和签名。

  3. 每完成一两个就跑判分:

    bash
    PYTHONPATH=python python -m pytest python/tests/test_w0p_python_math.py -q

    输出里 solutions 参数组应全绿(参考解回归),exercises 参数组的 skipped 随你的进度减少。全部 passed、0 skipped 时本章判分通过。

  4. 卡住了对照 python/labs/w0p_solutions.py,但先合上它自己写一遍——第3章的 Value 没有参考解可抄。

通过条件:exercises 参数组 37 项全 passed、0 skipped;能口述 2×2 矩阵乘和差商公式;能在不看资料的情况下写出 Scalar__add__ 和一个记住外层变量的闭包;能手算偏导数和梯度向量;能手算条件概率和贝叶斯更新;能手算熵和交叉熵;能手算梯度下降一步和 ReLU 前向传播。

故障注入与预期信号

本章只有这一张权威故障表;每个故障都能在判分测试里复现信号。

注入预期失败信号修复后证据
可变默认参数 def f(seen=[])多次调用之间 seen 残留上次的数据,重复调用测试偶发失败改为 seen=None + 函数体内初始化,重复调用结果一致
列表当值传递(b = a 后改 b原列表被连带修改,assert 对比失败list(a)a[:] 拷贝后再改,原列表不变
循环里定义闭包,晚绑定所有返回的函数记住循环变量的最终值用工厂函数或默认参数固定当前值,各闭包独立
__add__self.data += ...原对象被改写,(a+b)*a 组合运算结果漂移dunder 返回新对象,操作数在运算前后不变
broadcasting 不写 keepdims=True(N,M) - (N,)operands could not be broadcast togethermean(axis=1, keepdims=True),每行均值归零
矩阵乘中间维不等ValueError: matmul: dimension mismatch先过 matmul_shape 规则,(M,K)@(K,N)
差商 h 取 1e-12与真值比较误差 >1e-4,全是浮点噪声h≈1e-5x=2 处误差回到 1e-4 内
包装上系统 Python 而非 venvpytest 里 ModuleNotFoundError: numpywhich python 指向 .venv,activate 后再 install
偏导数求错变量(对 x 求导忘了把 y 当常数)partial_derivative(f, 1, 2, 'x') 结果偏离真值求 ∂f/∂x 时把 y 固定为传入值,只让 x 变
点积向量长度不等IndexError 或结果偏短assert len(a) == len(b)
矩阵乘内层索引顺序反了结果变成转置矩阵记忆 out[i][j] = 行i · 列j
贝叶斯公式分子分母搞反`P(HE)算成P(E
categorical 采样概率和 ≠ 1np.random.choiceprobabilities do not sum to 1先用 probs /= probs.sum() 归一化
熵函数忘了 p=0 时 0·log(0)=0ValueError: math domain errorif p > 0 过滤零概率项
KL 散度 q 为 0 而 p > 0ZeroDivisionErrorlog(0) 报错Q 在 P>0 处也必须 >0,计算前检查
perplexity 输入负数2^负数 结果 < 1 没有物理意义CE loss ≥ 0,perplexity = 2^CE
梯度下降符号错误(+= 而非 -=参数沿梯度方向爬升,损失单调递增记住 θ ← θ - η·∇L,梯度方向是上坡
动量速度忘记初始化第一次更新时 v 是随机值,结果不可复现v 初始化为 0,和 PyTorch SGD(momentum) 一致
ReLU 导数在 0 处dy/dx = x > 0 ? 1 : 0,0 处不连续但 subgradient 可定义实际框架用 x >= 0 约定,手工求导时注明

论文与延伸

本章无论文。延伸只看两个,且都不必现在读完:

  • Python 官方教程:本章最小集之外的概念以它为准查,不系统读。
  • Karpathy 的 micrograd:第3章的参照物,本章只看 Value 类的 __add__/__mul__ 长什么样,确认你能读懂——读懂即毕业。

前端/Agent 迁移

  • Python 的模块导入、venv 隔离、pytest 断言,对应你熟悉的 ESM、node_modules 隔离、Vitest——工具链心智模型整体平移,别把它当新学科。
  • 闭包记状态、不可变返回值(dunder 返回新对象)和你在 React/Vue 里学的"不直接改 props/state"是同一条纪律:第3章的梯度累加 += 之所以要单独强调,正因为它是这条纪律里少数"故意可变"的例外。
  • 读 shape 再动手,相当于前端接手接口数据先打印结构;Agent 时代则是先看 tool schema 再拼参数。
  • 偏导数和链式法则 = Vue computed 的依赖追踪:每个派生值知道自己的直接依赖,上游变了就重算,多条路径的贡献要累加。
  • 点积 = CSS 颜色匹配:两个颜色向量越对齐,点积越大;attention 的 Q·K 就是这个原理——越相关的 token 对,点积越大。
  • 矩阵转置 = React 的 key 列表转对象:行变列、列变行,同一批数据换个访问角度。
  • 条件概率 = 已知用户来自移动端后的转化率:分母从"所有访问"变成"移动端访问",重新定义"世界"。
  • 贝叶斯定理 = A/B 测试的信念更新:先验是测试前的判断,似然是看到的数据,后验是更新后的判断。
  • softmax 输出 = categorical 分布 = 分类器输出的概率分布;temperature 调节分布的尖锐度。
  • MLE = 从用户行为数据反推"最可能的偏好参数"。
  • 自信息 = console.log 的 surprise 程度:"bug fixed" 信息量小,"宇宙崩溃" 信息量大。
  • 熵 = 一组按钮点击分布的不确定性:越均匀越不可预测,越确定熵越低。
  • 交叉熵 = 期望行为 vs 实际行为的"信息差距":第4章的 CE loss 衡量模型预测和真实标签的信息差。
  • KL 散度 = 两个组件样式集的差异度量:从样式 P 切换到样式 Q 需要多少"信息调整"。
  • Perplexity = 平均每次预测要猜多少次才能命中:PP=2 说明平均 2 选 1,PP=1 说明百发百中。

口述与自测(不看资料,5–10 分钟)

  • 写出 Scalar__init____add__,说明 self 是什么、为什么 __add__ 要返回新对象而不是改 self.data
  • make_counter,解释内层函数为什么在外层返回后仍然记得 n,以及 nonlocal 少了会报什么错。
  • 手算 [1234] @ [5678],并说出 (B,T,C)@(C,C') 的输出 shape。
  • 用差商公式解释第3章的 gradcheck 在对拍什么;h 为什么不能取 1e-12。
  • 解释为什么交叉熵里用 -log(p) 而不是直接用 p 当损失。
  • f(x,y)=x2+3xy+y2(1,2) 处口算 f/xf/y;说明梯度向量的方向意味着什么。
  • 说同理链式法则的"累加而非覆盖"规则,并关联到 Vue computed 的依赖追踪。
  • 解释点积 ab=|a||b|cosheta 中 $ heta=90°$ 时为什么点积为 0,以及这对 attention 的 Q·K 意味着什么。
  • 手算 P(A|B) 并说明分母为什么变小;说明独立事件 P(A∩B) = P(A)·P(B) 的含义。
  • 口述贝叶斯定理的三个部分(先验/似然/后验),并举例说明 A/B 测试中如何更新信念。
  • 解释为什么 softmax 输出的向量是一个 categorical 分布,temperature 高/低时分布长什么样。
  • 抛硬币 10 次 7 次正面,写出 MLE 估计 p-hat 的公式并口算结果。
  • 手算 -log₂(0.25) 并说明为什么罕见事件信息量大;解释为什么均匀分布熵最大。
  • 口算 H([0.5, 0.5]) 和 H([1.0, 0.0]),说明为什么确定事件熵为 0。
  • 手算 cross-entropy([1, 0], [0.8, 0.2]) 并说明这等于 -log(0.8);解释为什么 CE 是"信息差距"。
  • 说同理 KL(P, Q) = 0 意味着两个分布完全一样,以及 KL 不对称的含义。
  • 解释 perplexity = 2^H 为什么和 cross-entropy 是单调关系,PP=1 意味着什么。
  • 手算梯度下降一步:θ=5, ∇L=2, η=0.1 → θ 更新为多少;解释 η 太大/太小的后果。
  • 解释动量的物理直觉:速度 v 如何累积历史梯度,β=0.9 意味着什么。
  • 写出 ReLU 的数学定义和导数;说明为什么 ReLU 会引发"神经元死亡"问题。
  • 用手写代码跑一遍 mlp_layer_forward([1,2], W, b, relu),说出 Wx+b 的维度规则。

资源 / 成本 / 隐私

本地 Python/NumPy、CPU 即可;gross cost 为 0。练习只用合成数值,不涉及外部数据、账号或隐私信息。

实验与参考

动手实验

目标:把"会用 Python 写类、闭包、NumPy"从口头说法变成判分记录。

环境准备

bash
cd <仓库>
python3 -m venv .venv && source .venv/bin/activate
python -m pip install -r requirements.txt
export PYTHONPATH="$PWD/python"

命令与预期输出

bash
python -m pytest python/tests/test_w0p_python_math.py -q
text
# 完成前(练习全是 NotImplementedError):
37 passed, 37 skipped

# 完成后(你的 exercises 全实现):
74 passed

# 判定条件:
# - solutions 参数组始终 37 passed(参考解回归)
# - exercises 参数组从 37 skipped 降到 0 skipped、37 passed

概念图

概念图一:前置知识依赖

概念图二:第2章 → 第21章 全书知识流

交互:第2章概念图探索

点击左侧节点,查看该概念的定义、对应第2章函数、后续章节的使用场景和前端类比。

Python
数学
神经网络

Python 最小集

定义

变量、列表推导式、函数默认参数、类与 dunder 方法、闭包、try/except、with。第3章写 Value 类直接消费这些。

对应第2章函数
后续周使用
使用场景
第3章Value.__add__/__mul__、_backward 闭包
第4章语言模型前向传播、训练循环
第6章Parameter 类、优化器
第8章nn.Module forward
前端类比

Python 的模块导入和 venv 隔离对应 ESM + node_modules;闭包记状态 = React/Vue 不直接改 state

学习路径前瞻

本章每个概念在后续章节中首次被消费的位置。学习时只需知道"这个概念以后会用到",不需要理解具体用法——遇到时再回看本章即可。

概念首次消费章节关系
Python 闭包 / nonlocal第3章Value._backward 用闭包记住输入节点
偏导数 ∂f/∂x第3章Value.backward() 对每个权重求偏导
链式法则(多变量)第3章反向传播的系统化执行;共享节点梯度累加
梯度向量 ∇f第6章SGD/AdamW 沿 L 更新参数
矩阵乘法 / 转置第6章embedding X @ W、attention Q @ K^T
点积第7章Q·K 衡量 query 和 key 的对齐度
条件概率 P(A|B)第4章Bigram 条件概率 P(wt|wt1)
贝叶斯定理第11章RAG 检索分数 = 贝叶斯后验更新
MLE 估计第4章训练目标 = 最大化数据似然 = 最小化 CE
自信息 I(x) = −log P(x)第4章−log(p) loss 的直觉来源
熵 H(X)第4章衡量语言模型输出不确定性;perplexity = 2^H
KL 散度 D_KL(P|Q)第10章DPO 隐式约束模型不偏离先验
交叉熵 H(P, Q)第4章CE loss = 最小化 KL(P, Q)(one-hot 时)
损失曲面 / 梯度下降第6章SGD/AdamW 优化器的几何直觉
动量(Momentum)第6章SGD+Momentum 克服峡谷震荡
ReLU 激活第8章Transformer FFN 的核心组件

Evidence

仓库当前机器证据(只读快照)

本章机器证据:evidence/02-runtime-v1.json。这是当前 checkout 的脱敏机器运行记录,只覆盖该 JSON 记录的命令、指标、产物和已知失败;它证明参考解与判分测试全绿,不能推出任何学习者已完成本章。本章是入门坡道,不在 clean_room/contracts.json 内;本文件已登记进 evidence/module-manifest-v1.json

学习者提交模板(待填写,不是当前机器证据)

复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actualartifacts 尤其不能被当作已运行或已通过。

yaml
schema: learn-llm.evidence.v1
module: 02-python-math-ramp
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 0
commands:
  - PYTHONPATH=python python -m pytest python/tests/test_w0p_python_math.py -q
metrics:
  - name: exercises_tests_passed
    expected: 16
    actual: <recorded-value>
  - name: exercises_tests_skipped
    expected: 0
    actual: <recorded-value>
  - name: difference_quotient_abs_error_cubic_at_2
    expected: <1e-4
    actual: <recorded-value>
artifacts:
  - <learner-repo-relative-artifact-path>
cost:
  gross_usd: 0
  credit_usd: 0
licenses:
  - source: <source>
    version: <version>
    license: <license>
    attribution: <attribution>
    redistribution: <redistribution>
known_failures:
  - <sanitized-failure-or-none>

判分未全绿或 skipped 不为 0 时,本章保持 gate,不要进入第3章。

下一步

进入 第3章 · 从导数到自动微分:你会立刻用到本章的 __add__、闭包和差商——Value 就是 Scalar 加上 .grad_backward

私有学习站 · 原理从零构建 · 勿提交个人隐私或密钥