Appearance
第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/except、with。 - 能建虚拟环境、
pip install、跑pytest -q并读懂断言失败输出。 - 能用 NumPy 建数组、读
.shape、索引、broadcasting、用@做矩阵乘。 - 能手算一个 2×2 矩阵乘、用中心差商近似导数、手算偏导数和梯度向量、计算条件概率和贝叶斯更新、从抛硬币数据做 MLE。
- 理解 Bernoulli / Categorical / Gaussian 三个分布的含义和在 LLM 中的位置。
本章不教什么
教多了就是跑偏。以下主题第3–10章不直接消费,本章一律不碰:
- Python:装饰器、生成器、异步(
async/await)、类型标注进阶、面向对象进阶(继承/多态)、包发布。 - 数学:SVD、极限的严格定义、任何证明。
学的时候遇到这些概念,记下名字,跳过,继续走主线。
公式与 shape
中心差商(第3章的有限差分直接用它):
矩阵乘的 shape 规则(第6章起反复使用):
| 对象 | 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 -qpytest 的断言就是裸 assert:断言失败时 pytest 会展开左右两边的值给你看,不需要学断言库。
常见错误:
- 包装上系统 Python 而不是 venv:
pip install之前先source .venv/bin/activate,用which python确认指向.venv。 - 在仓库根目录跑测试忘了
PYTHONPATH=python:import labs会ModuleNotFoundError。
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_evens、count_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_divide、difference_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.Module 的 forward 也是 __call__ 模式。
练习:Scalar、Linear。常见错误:
__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_multiplier、make_counter。常见错误:循环里定义闭包的晚绑定——所有闭包记住的是循环变量的最终值,用工厂函数或默认参数把当前值固定下来(JS 的 var 循环同款坑,let 没有,Python 的循环变量像 var)。
异常 try/except 与 with
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/catch,raise 对应 throw;with 在 JS 里没有对应物,课程后面读语料、读 checkpoint 都用它。
练习:read_first_line、matmul_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_column、normalize_rows、matmul、matmul_shape。常见错误:(N, M) 减 (N,)——你以为在按行减,实际 shape 对不上直接报错(或更糟,维度恰好兼容时静默算错);要 keepdims=True 拿到 (N, 1)。
数学最小集
矩阵乘 = 每一行对每一列做加权求和
手算这一个 2×2,算对就够:
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)=x³ 在 x=2 处,真值是 12;取 h=1e-5 算出来的差商误差在 1e-4 以内。第3章的 gradcheck 就是用这个公式对拍你手写的反向传播——两边对不上,就是你的 _backward 闭包写错了。h 不能取太小:1e-12 量级时分子只剩浮点噪声。
练习:difference_quotient。
对数、指数与期望的直觉
只需要三条直觉,第4章直接消费:
- log 把乘法变加法:
log(a·b) = log a + log b。一串概率连乘会小到浮点下溢,取 log 后变成连加,数值稳定。 - 概率越小,
-log(p)越大:p=1时为 0,p=0.1时约 2.30,p=0.01时约 4.61。模型给正确 token 的概率越低,惩罚越重——这就是交叉熵损失的来源。 - 期望 = 按概率加权的平均:
E[x] = Σ pᵢ·xᵢ,就是你算过的加权平均,权重是概率、加起来等于 1。
练习:neg_log_likelihood。常见错误:概率越小的词 -log(p) 越大,惩罚越重;p=1 时为 0,p=0.1 时约 2.30,p=0.01 时约 4.61。
偏导数:对多变量函数"固定其他变量求变化"
单变量导数你已经熟悉:
和单变量导数的区别只有一个:多了一个变量名。求
例:
第3章的 Value.backward() 本质上就是对每个权重求偏导数,然后把它们串起来。
几何直觉:二元函数
梯度向量 ∇f
把两个偏导数打包成一个向量,就是梯度:
梯度的方向是函数增长最快的方向,反方向就是下降最快的方向。第6章的梯度下降就是沿着
前端类比:CSS 的 linear-gradient(90deg, red, blue) 里 90deg 就是梯度方向——从纯色(低变化)到最颜色变化最快的方向。想象你在颜色山上,梯度告诉你往哪个方向走颜色变化最剧烈。
链式法则(多变量版)
第3章的反向传播就是多变量链式法则的系统化执行。对
关键变化:共享节点的梯度要累加,不能覆盖。第3章的 a + a(同一个节点被用两次)就是这条规则的试金石。
前端类比深化:Vue 的 computed 属性 A 依赖 B 和 C,B 变了要重算 A,C 变了也要重算 A——两次"变化信号"到达 A 时是独立的,互不覆盖,合起来才是 A 的真正变化。
练习:partial_derivative、gradient_vector、chain_rule_2layer。常见错误:对
向量 = 有序的数字列表(深化)
向量就是 [0.1, -0.7, 1.3] 这样一排有顺序的数字。顺序有意义:交换两个分量就是另一个向量。两个向量可以做三种基本运算:
- 数乘:
——把向量拉伸 倍 - 加法:
——首尾相接平移 - 点积(内积):
——一个数字,衡量"方向有多一致"
第4章的 embedding 表里,每一行就是一个词对应的向量。
点积:衡量两个向量的"默契度"
(同向)→ → 点积最大(完全一致) (垂直)→ → 点积为 0(正交,毫无关系) (反向)→ → 点积最小(完全相反)
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 手算,这里补两个关键性质:
转置:把矩阵的行和列互换,
转置和矩阵乘的关系:Q@K^T 时会直接用到。
非方阵乘法:现实中的矩阵几乎都不是方阵。
- Embedding 查询:
——把 token id 变成向量 - Attention score:
——每个位置看每个位置 - FFN:
——升维再降维
共同规律:
练习:matmul_2d、transpose。常见错误:内层循环的索引顺序反了导致结果转置;方阵假设导致非方阵报错。
特征值与特征向量(直觉级)
矩阵
如果结果只是把
直觉比喻:你站在一张弹性网上,往某个方向推——网主要沿着这个方向拉伸或收缩。那些"主要拉伸方向"就是特征向量,拉伸倍数就是特征值。
:该方向被拉伸 :该方向被压缩 :方向反转
第9章讨论 KV Cache 压缩和低秩分解时会遇到这个概念。本章不要求你手算特征值,能理解"矩阵变换中的主方向"就够了。
练习:本节无独立练习(特征值属选读)。matmul_2d 和 transpose 已经覆盖了核心操作。
条件概率:在已知条件下重新估算
你已经学过
直觉:把分母从"所有可能"缩小到"B 发生的那些"。就像把世界重新定义为"只有 B 是真的"。
例:掷一个公平骰子,
独立事件
如果 A 的发生不影响 B,反之亦然,称两者独立:
独立事件的链式法则最简:把每个概率直接乘起来。第4章的 naive Bayes 语言模型假设词与词之间独立(简化假设,现实中不成立但效果好)。
贝叶斯定理:看到数据后更新信念
贝叶斯定理是"先有猜测、再看证据、修正猜测"的数学表达:
:先验——看数据前的信念(历史概率) :似然——如果假设 H 成立,看到证据 E 的概率 :后验——看完证据后的更新信念
前端类比:A/B 测试里,你先认为版本 A 和 B 各有 50% 可能更好(先验);收集到 1000 个用户的点击数据后(证据),更新为"B 有 80% 概率更好"(后验)。第11章 RAG 检索也是这个思路——每个文档都有一个"相关"概率,检索结果更新这个概率。
常见分布:自然界的数据模板
概率分布描述"随机变量取各个值的概率长什么样"。
Bernoulli(伯努利):抛一次硬币,结果 0 或 1。
第4章预测下一个词是否为某个特定词,就是 Bernoulli 判断。
Categorical(分类/多项式):掷一个 K 面的骰子,结果
第4章的 softmax 输出就是一个 categorical 分布——每个词都有一个概率,加起来为 1。temperature 参数直接调节这个分布的"尖锐度"。
Gaussian(正态/高斯):钟形曲线,实数轴上最常见的分布。
:中心(均值) :宽度(标准差)
第6章的权重初始化常用 Xavier/Gaussian,第10章的 KL 散度约束模型输出不要偏离先验太远。
MLE 最大似然估计:什么参数最"合理"?
给定一组观察数据,MLE 问:哪个参数值让这些数据最可能发生?
例:抛硬币 10 次,出现 7 次正面。最可能的真实正面概率
似然函数:
更一般地,如果数据是
第4章的关键桥梁:语言模型的交叉熵 loss
前端类比:MLE 就像从用户行为数据里反推"用户最可能的偏好参数是什么"——你看到 70% 的点击在左侧按钮,就推断用户偏好左侧布局的概率是 0.7。
练习:conditional_probability、bayes_theorem、bernoulli_pmf、categorical_sample、mle_bernoulli。常见错误:贝叶斯公式里把
自信息:越不可能的事,信息量越大
如果一个事件发生的概率是
(必然事件)→ 比特(没有信息量) → 比特 → 比特
直觉:越罕见的事件,发生时带来越多的" surprise"。第4章的 -log(p) 损失就是这个原理——模型给低概率词高惩罚。
前端类比:console.log("bug fixed") 不意外(信息量小),console.log("宇宙崩溃") 极意外(信息量大)。
熵:不确定性的度量
一个随机变量
- 均匀分布(所有结果等可能)→ 熵最大(最不确定)
- 确定分布(只有一个结果概率为 1)→ 熵为 0(完全确定)
例:公平硬币
第4章用熵衡量语言模型输出的"不确定性";perplexity 就是
练习:entropy。常见错误:忘记
KL 散度:两个分布有多不同
KL 散度(Kullback-Leibler divergence)衡量分布
:P 和 Q 完全相同 :P 比 Q 更" surprised" - 不对称:
第10章的 DPO 训练就是最小化模型输出分布与参考分布之间的 KL 散度。
前端类比:两个组件样式(CSS 变量集合)的差异度量——值越不同,KL 散度越大。
交叉熵:预测 vs 真实的"信息差"
交叉熵 combines 熵和 KL 散度:
:真实标签分布(one-hot:正确词概率为 1,其余为 0) :模型预测分布(softmax 输出) - 训练目标:让
逼近 ,使交叉熵最小
第4章的 cross-entropy loss 就是
前端类比:你写了一个组件,期望行为是 P,实际行为是 Q——cross-entropy 衡量"期望"和"实际"之间的信息差距。
练习:self_information、entropy、cross_entropy、kl_divergence。
Perplexity:模型每步需要多少"猜测"
可以理解为"模型每预测一个词平均需要试多少次才能猜对"。
- PP = 1:模型完美预测(每次一次命中)
- PP = 100:模型平均要在 100 个词里猜一个
第4章的语言模型评测直接用 perplexity——越低越好。它和 cross-entropy 是单调关系:
前端类比:你写 UI 组件时平均要试几次才能让按钮出现在正确位置——perplexity 越低说明你对布局越有把握。
损失曲面 = 地形图
想象你站在一座山上,目标是走到最低点(山谷)。这座山的形状就是损失曲面——每个点的坐标是
- 平原:损失曲面平坦,梯度接近 0,参数更新很小
- 陡坡:梯度很大,参数更新大步跳跃
- 峡谷:一个方向陡、另一个方向缓——SGD 容易在陡的方向震荡
- 局部低谷:不是全局最低,但比周围低——SGD 可能卡在这里
第6章的 loss landscape 可视化就是把这个高维曲面压到 2D 让你看到。
梯度下降:蒙眼下山
梯度下降是最朴素的优化算法:站在当前位置,往梯度反方向走一步:
:当前位置(参数值) :梯度(山最陡的上坡方向) :最陡的下坡方向 :学习率——每步迈多大
前端类比:
- 蒙眼下山 = 梯度下降:你只能感觉到脚下地面的倾斜方向,不能看全貌
- 步长
= 每步迈多大:太大容易越过最低点(震荡),太小走得太慢 - 峡谷地形 = SGD 的困境:陡的方向 Update 很大,缓的方向 Update 很小——需要自适应步长
学习率的影响:
太大:在最低点两侧来回跳跃,甚至 diverges(损失爆炸) 太小:收敛极慢,可能卡在局部低谷 - 实际训练:用 learning rate schedule——训练初期大步快走,后期小步微调
动量:带惯性的下山
纯 SGD 在峡谷里容易"之"字形震荡。动量(Momentum)给参数更新加一个惯性项:
:速度(惯性 accumulated 历史梯度方向) :摩擦系数(通常 0.9),控制惯性保留多少
直觉:球从山上滚下来,遇到小坑不会卡住——惯性带着它冲过去。第6章的 SGD+Momentum 就是这个思想。
前端类比:滚动惯性 = iOS 列表滚动的"橡皮筋"效果——手指离开后,滚动不会立刻停止,而是带着惯性继续滑一段。
练习:gradient_descent_step、momentum_update。常见错误:学习率太大导致 NaN;动量方向符号搞反(梯度是上坡,更新应该是下坡)。
感知器:神经网络的原子单元
感知器(Perceptron)是最简单的神经网络单元:
:权重(每个输入的重要性) :偏置(阈值偏移) :激活函数(引入非线性)
如果
MLP:多层感知器
把多个感知器层叠起来,就得到 MLP(Multi-Layer Perceptron):
输入层 → 隐藏层₁ → 隐藏层₂ → … → 输出层每一层都是
为什么需要非线性激活? 如果所有层都是线性(
前端类比:
- 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 串起来,就是这个循环。
练习:relu、mlp_layer_forward。常见错误:忘了 ReLU 把负值截成 0;矩阵乘法 shape 不匹配。
从零实践
按「环境与工具」建好 venv、装好 numpy 和 pytest。
打开
python/labs/w0p_exercises.py,按上面各节的顺序把raise NotImplementedError逐个换成你的实现;不要改函数名和签名。每完成一两个就跑判分:
bashPYTHONPATH=python python -m pytest python/tests/test_w0p_python_math.py -q输出里
solutions参数组应全绿(参考解回归),exercises参数组的 skipped 随你的进度减少。全部 passed、0 skipped 时本章判分通过。卡住了对照
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 together | mean(axis=1, keepdims=True),每行均值归零 |
| 矩阵乘中间维不等 | ValueError: matmul: dimension mismatch | 先过 matmul_shape 规则,(M,K)@(K,N) |
差商 h 取 1e-12 | 与真值比较误差 >1e-4,全是浮点噪声 | h≈1e-5,x³ 在 x=2 处误差回到 1e-4 内 |
| 包装上系统 Python 而非 venv | pytest 里 ModuleNotFoundError: numpy | which 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(H | E)算成P(E |
| categorical 采样概率和 ≠ 1 | np.random.choice 报 probabilities do not sum to 1 | 先用 probs /= probs.sum() 归一化 |
| 熵函数忘了 p=0 时 0·log(0)=0 | ValueError: math domain error | 用 if p > 0 过滤零概率项 |
| KL 散度 q 为 0 而 p > 0 | ZeroDivisionError 或 log(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少了会报什么错。 - 手算
@ ,并说出 (B,T,C)@(C,C')的输出 shape。 - 用差商公式解释第3章的 gradcheck 在对拍什么;
h为什么不能取 1e-12。 - 解释为什么交叉熵里用
-log(p)而不是直接用当损失。 - 对
在 处口算 和 ;说明梯度向量的方向意味着什么。 - 说同理链式法则的"累加而非覆盖"规则,并关联到 Vue
computed的依赖追踪。 - 解释点积
中 $ 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。练习只用合成数值,不涉及外部数据、账号或隐私信息。
实验与参考
- 关联概念:术语表 - 有限差分 / gradcheck、术语表 - softmax、术语表 - 交叉熵 CE、术语表 - 偏导数 / 梯度、术语表 - 点积 / 矩阵乘、术语表 - 熵 / KL 散度 / Perplexity(后三个是本章新增词条,第4章/第10章直接消费)
动手实验
目标:把"会用 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 -qtext
# 完成前(练习全是 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 沿 |
| 矩阵乘法 / 转置 | 第6章 | embedding X @ W、attention Q @ K^T |
| 点积 | 第7章 | Q·K 衡量 query 和 key 的对齐度 |
| 条件概率 P(A|B) | 第4章 | Bigram 条件概率 |
| 贝叶斯定理 | 第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。
学习者提交模板(待填写,不是当前机器证据)
复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actual 和 artifacts 尤其不能被当作已运行或已通过。
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。