讲义定位
这一课是整个 Mini-TabPFN 的地基。后面的 Attention、Column Encoder、Row Token、Row-Level ICL 和 Prediction Head 都默认我们已经明确回答:模型的输入究竟是什么,它要预测的对象又是什么?
核心直觉:学的是「给定 context 如何预测 query」的程序,不是一张固定表的 \(x\to y\)。
本讲义先区分普通监督学习、task、episode、context/query 与 batch,再把它们落成可被代码、测试 共同检查的数据契约。课程受 TabPFN-3 的公开计算对象启发,但不复现官方参数规模、训练 recipe 或 benchmark 数字。
本课符号(首次落地)
| 符号 | 含义 |
|---|---|
| \(N_c, N_q\) | context / query 的 row 数(与 \(N_{\mathrm{context}}, N_{\mathrm{query}}\) 同义) |
| \(P\) | feature / column 数(论文图注常用 \(C\)=columns;课程用 \(P\) 以免与类别数冲突) |
| \(B\) | episode batch |
| \(M_{\mathrm{context}}, M_{\mathrm{query}}\) | row padding mask,\(1\)=有效 |
1. 学习目标与先修知识
学完本讲义后,你应该能够:
- 解释固定数据集泛化与未见任务泛化为什么是两个不同的问题;
- 用统一记号写出 context、query、episode 和 batch 的形状;
- 说明为什么 query label 可以用于 evaluation,却不能进入 prediction input;
- 写出同时支持分类和回归的
Episode数据对象; - 设计可 replay 的 task sampler,并按 task 而不是按 row 切分训练/测试;
- 通过 shape、dtype、label range 和 leakage tests 证明数据契约没有被破坏。
先修只需要 Python、基本概率和 PyTorch tensor。你不需要先会 Transformer。
2. 从普通监督学习开始
最熟悉的监督学习:给定 \(D_{\mathrm{train}}=\{(x_i,y_i)\}\),学习 \(\theta\),使 \(f_\theta(x)\) 在同一任务 分布的测试样本上更好。经验风险目标常写成:
这里数据集通常是一个相对固定的世界:列语义、标签空间、生成规律和 train/test 划分都已给定。随机 打乱 row 再切分,测试集仍属于同一张表或同一任务。
这并不是错误;它只是和 PFN 风格学习的对象不同。
2.1 三个容易混淆的层级
| 层级 | 它是什么 | 例子 |
|---|---|---|
| row / sample | 一次观测 | 一位病人的 12 个特征与一个标签 |
| dataset / table | 一组来自同一生成机制的观测 | 一张固定医疗表 |
| task | 规定「如何生成一张表以及如何预测」的完整问题 | 一个分界方向、噪声水平、标签编码 |
普通监督学习常把 dataset 当作主要边界;PFN 训练则把 task 放到主要边界上。模型要学的是:给我 一个新任务的少量已标记观测,我该如何利用它预测同一任务中的未知观测?
3. Task-conditioned prediction
对一个 task,先观察带标签的 context,再给出只有特征的 query。模型输出的是条件预测分布:
与论文叙述的对应:context ≈ training rows,query ≈ test rows。课程统一用 context/query,避免和 「模型预训练 / 下游微调」两套 train 词汇混在一起。
条件边界必须钉死:
- \(X_{\mathrm{context}}, Y_{\mathrm{context}}\):prediction 时允许看到的 task evidence;
- \(X_{\mathrm{query}}\):要回答的问题;
- \(Y_{\mathrm{query}}\):只在 loss / evaluation 时作为答案存在;
- \(Y_{\mathrm{query}}\) 不得进入
model.forward。
因此「模型输入」不再是一行独立的 \(x\),而是一个带 context/query 关系的 episode。
训练时 loss 的合法形状是:
3.1 为什么 context label 是合法信息?
假设 context 中有 (x_1, 0)、(x_2, 1)。这些标签告诉模型当前 task 的局部规律,例如分界面大概 在哪里。删掉 \(Y_{\mathrm{context}}\),模型只能靠 query feature 和预训练先验猜测。
这不是数据泄漏,而是任务定义的一部分:context label 是已观察到的 task evidence。真正的泄漏是把 query 的正确答案提前放进模型输入。
4. Episode、Batch 与 padding
一个最小 episode 的未 padding 接口:
task_kind 与 task_seed 是 provenance / control metadata,不是 feature,不能偷塞进 \(X\)。
批处理时不同 episode 的 \(N_c, N_q\) 可能不同,需要 padding:
Mini 简化:第一版课程在一个 batch 内固定 \(P\),只对 rows 做 padding。官方模型还要处理可变特征数、 特征子采样与多种预处理 estimator;本课不实现。
padding value 只是占位符;真正决定位置是否参与计算的是 mask。若用 0 padding 却没有 mask,模型 无法区分「真实 feature 恰好为零」和「这里没有 row」。
4.1 四个轴必须在代码中有名字
以后看到 \([B, N, P, D]\),必须能说清楚:
B = episode batch
N = row index(或 N_c / N_q)
P = feature / column index
D = learned representation width
轴语义不清会导致危险错误:代码仍能运行,但把 column attention 写成 row attention,或把不同 episode 的 row 互相混在一起。
5. 固定数据集泛化与未见任务泛化
两个实验都可能被称为「测试集表现」,但证明的能力不同:
若先把 100 个 task 的所有 rows 合并,再随机按 row 切分,测试 row 可能来自训练时已见过的 task。 模型即使没有真正学会「如何利用 context」,也可能依靠任务痕迹得到较好结果。
正确的 task-level split:
- 先采样 task latent state;
- 用该 state 生成 context/query;
- 按 task id 把完整 episode 放进 train / val / test;
- 确认 test task 的 latent state 没有被训练阶段复用。
5.1 一个最小反例
设每个 task 有 20 个 context row 和 5 个 query row,共 100 个 task。随机切 row 时,模型可能在训练 中已见过 task 17 的分界方向,只是没见过其中某个 row;按 task 切分时,测试才必须从 task 17 的新 context 重新推断规律。
因此:
- 「在固定任务内预测」是较弱、合法的陈述;
- 「学会了跨任务预测算法」需要 held-out task evidence,不能由 row-level split 单独支持。
6. Task latent state 与 sampler
Synthetic task 通常先采样不可直接输入模型的 latent state,例如:
w 线性方向
function 生成函数族
noise_scale 噪声强度
label_map 标签编码
再用 latent state 生成 feature 和 label。latent state 可以帮助解释数据来源,但不应被当成模型已 观察到的 feature;否则模型是在读取出题答案,而不是从 context 中学习。
这与 TabPFN 系「仅在合成 prior 上预训练、再在真实表上做 in-context 预测」的设定一致:参数学的是 跨任务的推断程序,单次 forward 里的适应来自读取 context。
6.1 分类任务
最小二分类:
x = torch.randn(n_context + n_query, n_features, generator=g)
w = torch.randn(n_features, generator=g)
score = x @ w
y = (score > 0).to(torch.int64)
随后可增加非线性或 label noise。每增加一个选项,都应记录在 TaskSpec 中。
6.2 回归任务
mean = x @ w + 0.4 * torch.sin(x[:, 0])
scale = 0.2 + 0.7 * x[:, 0].abs()
y = mean + scale * torch.randn(mean.shape, generator=g)
mean 是条件均值,scale 描述不确定性随输入变化。它们属于数据生成机制,不是可供模型直接读取的 额外输入。
7. 代码契约:数据对象先于模型
@dataclass(frozen=True)
class Episode:
x_context: Tensor # [N_c, P]
y_context: Tensor # [N_c]
x_query: Tensor # [N_q, P]
y_query: Tensor # [N_q], evaluation only
task_kind: Literal["classification", "regression"]
task_seed: int
def assert_contract(self):
assert self.x_context.ndim == 2 and self.x_query.ndim == 2
assert self.x_context.shape[1] == self.x_query.shape[1]
assert self.y_context.shape[0] == self.x_context.shape[0]
assert self.y_query.shape[0] == self.x_query.shape[0]
assert self.x_context.dtype == torch.float32
这个对象把「task-conditioned prediction」变成可检查边界。forward 最好只接收 x_context, y_context, x_query, masks;y_query 由 loss / evaluator 单独持有。
8. 为什么第一课不急着写 Transformer
Transformer 只能变换它收到的 token。若 episode 定义错了,仍可能在错误输入上得到很好的训练 loss:
- query label 被拼入 unified labels;
- task seed 被当成 feature;
- padding 没有 mask;
- train/test 是 row split 而不是 task split;
- classification label 被当成连续回归值。
这些错误不会都触发 shape error,因此必须先固定数据对象、测试和 sampler。
9. 课堂推导顺序
- 画 \(x\to y\) 的固定数据集图;
- 改成
context → query prediction的 episode 图; - 标出 prediction 时可见与不可见的变量;
- 写出 \(\mathbb{R}^{N_c\times P}\) 与 \(\mathbb{R}^{N_q\times P}\);
- 加入 padding 和 mask;
- 采样 20 个不同 task seed;
- 故意把 \(Y_{\mathrm{query}}\) 拼回输入,让 leakage test 失败;
- 修复后再进入第 2 课 Attention。
10. 知识检查
- 为什么 context label 是合法输入而 query label 不是?
- 一个 episode 与一张固定表的边界有什么不同?
- 若 \(N_c\) 在 batch 内不同,为什么需要
context_mask? - 为什么
task_seed是 provenance,而不是 feature? - 为什么 row-level random split 不能证明 held-out task generalization?
- 如果 classification generator 的 label map 改变,哪些对象必须同步变化?
11. 本课的硬结论
本课结束时,唯一可以声称的能力是:我们已经建立了一个可 replay、可检查、不会把 query answer 偷给 模型的 task/episode 数据接口。还不能声称模型已经学会跨任务预测;那需要后续模型、训练和 held-out task 评测共同证明。
第 1 课:项目介绍与任务表示
本课交付
本课结束时,学习者不是「知道 PFN 是在新任务上预测」,而是已经有一个可被后续模型直接消费的 Episode 对象,并能用同一个 sampler 产生 classification、regression、不同 feature 数和不同 context/query 大小的任务。
核心直觉:学的是「给定 context 如何预测 query」的程序,不是一张固定表的 \(x\to y\)。
本课的硬交付是:
data/task_types.py:Episode、TaskSpec、TaskKind;data/synthetic_classification.py与data/synthetic_regression.py;data/task_sampler.py:可重放的任务采样和 context/query split;models/mlp_baseline.py:只用于建立比较口径的 per-task baseline;tests/test_episode_contract.py:shape、dtype、label range、seed replay;- 一页实验记录:固定数据集泛化 vs 未见任务泛化。
本课符号
| 符号 | 含义 |
|---|---|
| \(N_c, N_q\) | context / query row 数 |
| \(P\) | feature 数 |
| \(B\) | episode batch |
| \(M_{\mathrm{context}}, M_{\mathrm{query}}\) | row padding mask |
项目问题
普通监督学习通常写成 \(x\to y\);PFN 风格模型的输入是带标签的 context 和一批未知 query:
训练 loss 的合法形状(canon §3.1):
如果这一层没有被固定,后面的 row token、label injection 和 ICL mask 都会失去明确对象。第一课 因此先不实现 Transformer,而是把「一个任务」「一个 episode」「一个 batch」区别开。
与论文:context ≈ training rows,query ≈ test rows。
3 小时课堂流程
| 时间 | 内容 | 课堂证据 |
|---|---|---|
| 00:00–00:20 | 从固定数据集训练切换到 task-conditioned prediction | 学习者画出两种计算图 |
| 00:20–00:50 | Task、Episode、Batch 与 context/query 记号 | 写出所有 tensor shape |
| 00:50–01:35 | 二分类与回归任务生成器 | 采样并打印一个 episode |
| 01:35–01:50 | 休息与错误样本检查 | 找到一个故意注入的 leakage |
| 01:50–02:25 | per-task MLP baseline | 产生 query prediction 和 baseline 指标 |
| 02:25–02:50 | replay、padding、mask 与 query 数量实验 | 四个 contract tests 通过 |
| 02:50–03:00 | 退出评审与作业说明 | 提交 artifact manifest |
1. 先固定四个对象
未 padding 的 episode:
批处理时允许 \(N_c, N_q, P\) 因 episode 而异;第一版课程实现先在一个 batch 内固定 \(P\),对 rows 做 padding:
Y_query 可以存在于 evaluation object,但任何 model.forward 的输入签名都不允许接收它。这个 约束从第一课写入测试,避免第五课才发现 query label 已沿 data loader 泄漏。
2. 课堂讲解:固定数据集泛化不是新任务泛化
前者可以把列语义、row identity 或固定分布记进参数;后者要求参数学习「如何使用 context」。因此 训练 split 的单位必须是 task/episode,而不是把同一张表随机切 row 后混入 train/test。
最小反例:生成 100 个任务,每个 20 context + 5 query。若先合并所有 rows 再随机切分,模型可能在 测试时看到同一任务的近邻;若按 task id 切分,才是 held-out task。
3. 代码接口:先写数据契约
# data/task_types.py
from dataclasses import dataclass
from typing import Literal
import torch
TaskKind = Literal["classification", "regression"]
@dataclass(frozen=True)
class Episode:
x_context: torch.Tensor # [N_c, P]
y_context: torch.Tensor # [N_c]
x_query: torch.Tensor # [N_q, P]
y_query: torch.Tensor # [N_q], evaluation only
task_kind: TaskKind
task_seed: int
def assert_contract(self) -> None:
assert self.x_context.ndim == 2 and self.x_query.ndim == 2
assert self.x_context.shape[1] == self.x_query.shape[1]
assert self.y_context.shape[0] == self.x_context.shape[0]
assert self.y_query.shape[0] == self.x_query.shape[0]
assert self.x_context.dtype == torch.float32
不要把 task_kind 或 task_seed 偷塞进 feature tensor。它们是 provenance / control metadata。
4. 生成两个最小任务族
4.1 二分类
def sample_classification_episode(
*, seed: int, n_context: int, n_query: int, n_features: int,
nonlinear: bool = False, flip_prob: float = 0.0,
) -> Episode:
g = torch.Generator().manual_seed(seed)
x = torch.randn(n_context + n_query, n_features, generator=g)
w = torch.randn(n_features, generator=g)
score = x @ w
if nonlinear:
score = score + 0.5 * torch.sin(x[:, 0])
y = (score > 0).to(torch.int64)
flips = torch.rand(y.shape, generator=g) < flip_prob
y = torch.where(flips, 1 - y, y)
return Episode(x[:n_context], y[:n_context], x[n_context:], y[n_context:],
"classification", seed)
4.2 回归
def sample_regression_episode(*, seed, n_context, n_query, n_features,
heteroskedastic=False) -> Episode:
g = torch.Generator().manual_seed(seed)
x = torch.randn(n_context + n_query, n_features, generator=g)
w = torch.randn(n_features, generator=g)
mean = x @ w + 0.4 * torch.sin(x[:, 0])
scale = 0.2 + (0.7 * x[:, 0].abs() if heteroskedastic else 0.0)
y = mean + scale * torch.randn(mean.shape, generator=g)
return Episode(x[:n_context], y[:n_context], x[n_context:], y[n_context:],
"regression", seed)
这里的 w、噪声和函数族是任务 latent state;它们可以用于生成数据,但不能作为 model input。
5. Guided lab:写一个不偷看 query 的 baseline
使用 PerTaskMLP:每个 episode 内只用 x_context/y_context 训练一个很小的 MLP,再在 x_query 上预测。它不承担跨任务 amortization,只提供「每个任务重新拟合」参照。
课堂必须实现并打印:
task_seed=17, kind=classification
context: X=[8, 3], Y=[8], labels={0, 1}
query: X=[4, 3], Y=[4]
baseline: accuracy=...
对照:
- 固定同一 episode,增加 query 数量,确认 prediction 接口不依赖固定 query size;
- 保持 generator 配置不变,仅改
task_seed,确认面对的是新任务而不是同一表的重排。
6. 必做验证与预期证据
tests/test_episode_contract.py 必须覆盖:
test_context_query_have_same_feature_width
test_query_labels_are_not_in_model_input
test_classification_labels_are_in_0_to_k_minus_1
test_same_seed_replays_exact_episode
故意把 y_query 拼回 input,再让测试失败一次;这次失败是教学证据,不要删测试来「通过」。
常见失败
- 把 episode 内的
task_seed、真实w或 query label 当成 feature; - 先合并所有任务再随机按 row 切 train/test;
- 用
0padding 却没有context_mask,把真实零值和 padding 混为一谈; - 只报告固定任务的 accuracy,却称模型学会了 PFN;
- 生成器只支持固定 \(P/N\),导致后续课程无法做 task distribution。
退出条件与评分
- 至少生成 20 个不同 task seed 的 classification 和 regression episodes;
- 同 seed 重放逐元素一致;
model.forward的输入中没有Y_query;- 固定任务和 held-out task 的评测表分开;
- 提交一页说明:baseline 在什么问题上有意不等价于 Mini-TabPFN。
评分建议:数据契约 30%,任务生成 25%,replay/leakage tests 25%,解释和 artifact 20%。
课后作业
加入三分类 generator,并让 TaskSpec 显式记录 n_classes、n_features、nonlinear、 noise_scale。不允许修改 Episode 的 row/feature 语义;下一课的 attention module 应能直接复用。