Skip to content

Commit f2d9b3d

Browse files
committed
📝 docs(user-manual): add comprehensive Chinese user manual and exp_fn contract template
- 新增《用户手册》中文文档,包含 exp_fn 契约、返回值状态矩阵、产物协议矩阵、开发流程与调试清单 - 新增 exp_fn 契约矩阵模板(exp_fn_contract_matrix.py),演示返回 dict/返回 None/SkipRun/异常失败四种场景 - 更新 README.md 添加用户手册与 API 参考的导航链接 - 完善 analyzer、pipeline、runner、types 模块的 docstring,增加返回值与使用示例说明 - 添加用户手册内容测试与模板冒烟测试
1 parent a1ec392 commit f2d9b3d

11 files changed

Lines changed: 439 additions & 10 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ analyzer.to_csv("./results_demo/summary.csv", sort_by=["model", "lr"])
189189
2. 自动生成首页 `index.md``reference/` API 页面;
190190
3. `mkdocstrings` 从类/函数 docstring 渲染参数、返回值与示例。
191191

192+
推荐先看用户手册,再查 API:
193+
194+
- [用户手册(开发流程与产物协议)](user-manual.zh.md)
195+
- [API 参考(函数与类型签名)](reference/)
196+
192197
本地入口:
193198

194199
- 生成脚本:[`scripts/gen_ref_pages.py`](https://github.com/ztxtech/ztxexp/blob/main/scripts/gen_ref_pages.py)

docs_src/user-manual.zh.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# 用户手册(中文)
2+
3+
本手册面向**直接使用 ztxexp 开发实验**的用户,而不是仅查询 API 参数。
4+
如果你只想知道函数签名,请看 API 参考;如果你想知道一个实验应该怎么落地、产物怎么组织、失败如何排查,请按本手册执行。
5+
6+
## 1. 先理解 `exp_fn` 契约
7+
8+
`ztxexp` 的单次实验函数固定签名:
9+
10+
```python
11+
def exp_fn(ctx: RunContext) -> dict | None:
12+
...
13+
```
14+
15+
### 1.1 `ctx` 里最关键的字段
16+
17+
- `ctx.run_id`:本次 run 的唯一 ID(通常也是目录名)。
18+
- `ctx.run_dir`:本次 run 的目录路径。
19+
- `ctx.config`:当前配置字典(已经是最终配置,不需要再从 argparse 解析)。
20+
- `ctx.logger`:当前 run 专属日志器,写入 `run.log`
21+
- `ctx.meta`:运行元数据(实验名、分组、标签、种子、环境采集信息等)。
22+
23+
### 1.2 `ctx.log_metric(...)` 用于过程指标
24+
25+
当你希望记录 step 级曲线(例如每个 epoch 的 loss/acc)时,使用:
26+
27+
```python
28+
ctx.log_metric(step=1, metrics={"loss": 0.92, "acc": 0.71}, split="train", phase="fit")
29+
```
30+
31+
这会写入 `metrics.jsonl`(每行一个事件),并触发已注册 tracker 的 `on_metric` 回调。
32+
33+
## 2. 返回值与状态矩阵(决定 run 成败)
34+
35+
`exp_fn` 只允许返回 `dict | None`。不同返回/异常对应的行为如下:
36+
37+
| 场景 | 你在 `exp_fn` 中做什么 | run 状态 | 关键产物 |
38+
| --- | --- | --- | --- |
39+
| 最终指标返回 | `return {"score": 0.93}` | `succeeded` | `metrics.json` |
40+
| 仅过程曲线 | `ctx.log_metric(...); return None` | `succeeded` | `metrics.jsonl`(无 `metrics.json`|
41+
| 主动跳过 | `raise SkipRun("reason")` | `skipped` | `run.json` + `events.jsonl`(skip 事件) |
42+
| 业务异常 | 抛出异常(如 `RuntimeError`| `failed` | `error.log` + `run.json.error_*` |
43+
| 非法返回值 | `return 123` 等非 `dict|None` | `failed` | `error.log``TypeError`|
44+
45+
### 2.1 关键判定规则
46+
47+
- 成功判定只看:`run.json.status == "succeeded"`
48+
- 不再使用旧版 `_SUCCESS` 文件。
49+
50+
## 3. 产物协议矩阵(该写什么、写到哪里)
51+
52+
每个 run 目录遵循 v2 协议,核心结构如下:
53+
54+
```text
55+
<results_root>/<run_id>/
56+
config.json
57+
run.json
58+
meta.json
59+
metrics.json # 可选
60+
metrics.jsonl # 可选
61+
events.jsonl # 可选
62+
artifacts/
63+
checkpoints/
64+
run.log
65+
error.log # 失败时
66+
```
67+
68+
| 产物 | 谁写入 | 何时出现 | 必选/可选 | 说明 |
69+
| --- | --- | --- | --- | --- |
70+
| `config.json` | 框架 | run 启动时 | 必选 | 当前 run 的最终配置快照。 |
71+
| `run.json` | 框架 | 启动时创建,结束时回填 | 必选 | 状态机文件,含 `status/start/finish/error` 等。 |
72+
| `meta.json` | 框架 | 启动时写入,可随重试更新 | 必选(v0.4+) | 复现与治理元数据。 |
73+
| `metrics.json` | 框架 | `exp_fn` 返回 `dict`| 可选 | 最终指标快照,适合排名/汇总。 |
74+
| `metrics.jsonl` | 框架 | 调用 `ctx.log_metric`| 可选 | step 级时间序列指标。 |
75+
| `events.jsonl` | 框架 | run 生命周期中 | 可选 | `start/retry/skip/error/end` 事件流。 |
76+
| `artifacts/` | 用户 + 框架创建目录 | run 启动时创建目录 | 必选目录 | 业务文件统一放这里(模型、图表、报告等)。 |
77+
| `checkpoints/` | 用户 + 框架创建目录 | run 启动时创建目录 | 必选目录 | 断点恢复文件建议统一放这里。 |
78+
| `error.log` | 框架 | run 失败时 | 可选 | 失败堆栈,优先排查入口。 |
79+
80+
## 4. 最终指标、过程指标、业务产物如何分工
81+
82+
- 最终指标(用于横向比较):`return dict`,自动落到 `metrics.json`
83+
- 过程指标(用于画曲线和诊断):`ctx.log_metric(...)`,落到 `metrics.jsonl`
84+
- 业务产物(模型、日志、图表、预测样本):手动写入 `artifacts/`
85+
- checkpoint(恢复训练):写入 `checkpoints/`
86+
87+
推荐做法:
88+
89+
1.`metrics.json` 只保留关键汇总指标(如 `best_val_f1``test_acc`)。
90+
2.`metrics.jsonl` 记录细粒度训练过程(每 step/epoch)。
91+
3. 把大文件和中间物全部放在 `artifacts/``checkpoints/`,不要污染 run 根目录。
92+
93+
## 5. 用户开发流程(从 0 到可分析)
94+
95+
### 5.1 构建配置
96+
97+
使用 `ExperimentPipeline``ExpManager` 构建参数空间:
98+
99+
```python
100+
from ztxexp import ExperimentPipeline
101+
102+
pipeline = (
103+
ExperimentPipeline("./results_demo", base_config={"seed": 42})
104+
.grid({"lr": [1e-3, 1e-2]})
105+
.variants([{"model": "tiny"}, {"model": "base"}])
106+
.exclude_completed()
107+
)
108+
```
109+
110+
### 5.2 编写 `exp_fn`
111+
112+
```python
113+
from pathlib import Path
114+
from ztxexp import RunContext
115+
116+
117+
def exp_fn(ctx: RunContext) -> dict | None:
118+
lr = float(ctx.config["lr"])
119+
model = str(ctx.config["model"])
120+
121+
# 过程指标
122+
ctx.log_metric(step=1, metrics={"loss": 0.8}, split="train", phase="fit")
123+
124+
# 业务产物
125+
artifact = Path(ctx.run_dir) / "artifacts" / "summary.txt"
126+
artifact.write_text(f"run={ctx.run_id}, model={model}, lr={lr}\n", encoding="utf-8")
127+
128+
# 最终指标
129+
return {"score": round(1.0 - lr, 4)}
130+
```
131+
132+
### 5.3 选择执行模式
133+
134+
- `sequential`:先保证正确性,再扩并发。
135+
- `process_pool`:CPU 密集任务优先考虑。
136+
- `joblib`:需要与 joblib 生态兼容时使用。
137+
- `dynamic`:实验特性,按 CPU 阈值动态提交。
138+
139+
### 5.4 分析与清理
140+
141+
```python
142+
from ztxexp import ResultAnalyzer
143+
144+
analyzer = ResultAnalyzer("./results_demo")
145+
df = analyzer.to_dataframe(statuses=("succeeded",))
146+
curve_df = analyzer.to_curve_dataframe(metric_key="loss")
147+
```
148+
149+
## 6. 调试与排错清单
150+
151+
1. 先看 `run.json.status`,再看 `error.log`
152+
2. 结果为空时检查:是否 `return dict`、是否被 `SkipRun`、是否被过滤条件排除。
153+
3. 曲线缺失时检查:是否调用了 `ctx.log_metric`
154+
4. `exclude_completed` 异常时检查:历史目录是否是 v2 协议且成功状态是 `succeeded`
155+
5. 并行场景异常先切 `sequential` 复现,再回到并行模式。
156+
157+
## 7. 复制即改:建议优先使用这些模板
158+
159+
- 契约矩阵模板(本手册对应):
160+
- `examples/template_library/basics/exp_fn_contract_matrix.py`
161+
- 模板库入口:
162+
- [示例模板库导航](examples-lib/index.md)
163+
- [模板索引表](examples-lib/catalog.md)
164+
- [场景复制矩阵](examples-lib/matrix.md)
165+
166+
## 8. 与 API 参考的关系
167+
168+
- 用户手册(本页):回答“如何用 ztxexp 开发完整实验”。
169+
- API 参考:回答“某个类/函数的参数、返回值和签名是什么”。
170+
171+
建议阅读顺序:
172+
173+
1. 先看本手册完成第一个可运行实验;
174+
2. 再按需跳转 API 页面查看细节参数。

examples/template_library/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
| `analysis/dataframe_csv_export.py` | DataFrame + CSV 导出 | 将 run 目录聚合为表格并导出 CSV。 |
1010
| `analysis/leaderboard_comparison.py` | 排行榜对比模板 | 快速生成 Top-K 配置列表,便于版本评审。 |
1111
| `analysis/pivot_excel_report.py` | 透视表 Excel 报告 | 按模型/超参数维度输出可读的透视表报告。 |
12+
| `basics/exp_fn_contract_matrix.py` | `exp_fn` 契约矩阵模板 | 一次性演示返回 dict / 返回 None / SkipRun / 异常失败四类结果与产物差异。 |
1213
| `basics/grid_and_variants.py` | 网格搜索 + 变体实验 | 同时遍历超参数网格和架构变体,适合 ablation 初期。 |
1314
| `basics/manager_runner_split.py` | 管理器与执行器解耦 | 当你需要先构建配置再交给不同 runner 时使用。 |
1415
| `basics/minimal_pipeline.py` | 最小可运行实验 | 用于快速验证环境、目录协议和基础执行链路。 |
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""`exp_fn` 契约矩阵模板。
2+
3+
场景说明:
4+
1. 用一个模板同时演示 `exp_fn` 的四种关键结果路径:返回 dict、返回 None、SkipRun、异常失败。
5+
2. 便于团队统一“返回什么、写到哪里、如何判定状态”的约定。
6+
7+
输入配置字段:
8+
- `scenario`:
9+
- `return_metrics`:返回最终指标字典,触发 `metrics.json`。
10+
- `stream_only`:仅写 step 指标流并返回 `None`。
11+
- `skip`:主动跳过(`SkipRun`),run 状态为 `skipped`。
12+
- `fail`:抛出异常,run 状态为 `failed` 并生成 `error.log`。
13+
- `lr`:示例超参数(可选)。
14+
15+
输出产物差异(由框架协议决定):
16+
- 所有场景都会有:`config.json`、`run.json`、`meta.json`、`events.jsonl`、`artifacts/`。
17+
- `return_metrics` 会额外写入:`metrics.json`。
18+
- `stream_only` 会写入:`metrics.jsonl`,通常没有 `metrics.json`。
19+
- `fail` 会写入:`error.log`。
20+
21+
复制后最少改动:
22+
1. 把 `exp_fn` 中伪指标替换为真实训练/评测逻辑。
23+
2. 保留 `scenario` 分支用于本地自测,或改成你的业务分支条件。
24+
3. 将你的模型、样本、报告统一写入 `ctx.run_dir / "artifacts"`。
25+
"""
26+
27+
from __future__ import annotations
28+
29+
import json
30+
from pathlib import Path
31+
32+
from ztxexp import ExperimentPipeline, RunContext, SkipRun
33+
34+
35+
def exp_fn(ctx: RunContext) -> dict[str, float] | None:
36+
"""演示 `exp_fn` 契约的四种典型分支。"""
37+
scenario = str(ctx.config.get("scenario", "return_metrics"))
38+
lr = float(ctx.config.get("lr", 0.001))
39+
40+
artifact_payload = {
41+
"run_id": ctx.run_id,
42+
"scenario": scenario,
43+
"config": ctx.config,
44+
"note": "replace with your real experiment artifacts",
45+
}
46+
artifact_path = Path(ctx.run_dir) / "artifacts" / f"{scenario}.json"
47+
artifact_path.write_text(
48+
json.dumps(artifact_payload, ensure_ascii=False, indent=2),
49+
encoding="utf-8",
50+
)
51+
52+
if scenario == "return_metrics":
53+
ctx.log_metric(step=1, metrics={"loss": 0.83}, split="train", phase="fit")
54+
return {
55+
"score": round(1.0 - lr, 4),
56+
"best_val_loss": 0.71,
57+
}
58+
59+
if scenario == "stream_only":
60+
ctx.log_metric(step=1, metrics={"loss": 0.92}, split="train", phase="fit")
61+
ctx.log_metric(step=2, metrics={"loss": 0.78}, split="train", phase="fit")
62+
return None
63+
64+
if scenario == "skip":
65+
raise SkipRun("Scenario skip: this config should be skipped by design.")
66+
67+
if scenario == "fail":
68+
raise RuntimeError("Scenario fail: intentional failure for contract demonstration.")
69+
70+
raise ValueError(f"Unknown scenario: {scenario}")
71+
72+
73+
if __name__ == "__main__":
74+
pipeline = (
75+
ExperimentPipeline(
76+
results_root="./results_templates/exp_fn_contract_matrix",
77+
base_config={"seed": 42, "task": "exp_fn_contract_matrix"},
78+
)
79+
.variants(
80+
[
81+
{"scenario": "return_metrics", "lr": 0.001},
82+
{"scenario": "stream_only", "lr": 0.005},
83+
{"scenario": "skip", "lr": 0.01},
84+
{"scenario": "fail", "lr": 0.02},
85+
]
86+
)
87+
)
88+
89+
summary = pipeline.run(exp_fn, mode="sequential")
90+
print(summary)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ theme:
2828

2929
nav:
3030
- 首页: index.md
31+
- 用户手册: user-manual.zh.md
3132
- 迁移指南: migration-v04.zh.md
3233
- 示例模板库: examples-lib/
3334
- API 参考: reference/

tests/test_docs_user_manual.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
ROOT = Path(__file__).resolve().parents[1]
6+
7+
8+
def test_user_manual_contains_exp_fn_contract_sections():
9+
manual_path = ROOT / "docs_src" / "user-manual.zh.md"
10+
assert manual_path.exists()
11+
12+
content = manual_path.read_text(encoding="utf-8")
13+
required_keywords = [
14+
"exp_fn(ctx: RunContext) -> dict | None",
15+
"返回值与状态矩阵",
16+
"产物协议矩阵",
17+
"SkipRun",
18+
"ctx.log_metric",
19+
"metrics.json",
20+
"metrics.jsonl",
21+
"error.log",
22+
]
23+
for keyword in required_keywords:
24+
assert keyword in content

tests/test_templates_smoke.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import uuid
55
from pathlib import Path
66

7-
from ztxexp import ExpRunner, RunContext, RunMetadata, utils
7+
import pytest
8+
9+
from ztxexp import ExpRunner, RunContext, RunMetadata, SkipRun, utils
810

911
ROOT = Path(__file__).resolve().parents[1]
1012
TEMPLATE_ROOT = ROOT / "examples" / "template_library"
@@ -84,3 +86,44 @@ def test_template_smoke_analysis_category(tmp_path, monkeypatch):
8486
module = _load_module(path)
8587
assert hasattr(module, "main")
8688
module.main()
89+
90+
91+
def test_exp_fn_contract_matrix_template(tmp_path):
92+
path = TEMPLATE_ROOT / "basics" / "exp_fn_contract_matrix.py"
93+
module = _load_module(path)
94+
assert hasattr(module, "exp_fn")
95+
96+
ctx_metrics = _make_ctx(tmp_path, {"scenario": "return_metrics", "lr": 0.001})
97+
try:
98+
result_metrics = module.exp_fn(ctx_metrics)
99+
assert isinstance(result_metrics, dict)
100+
assert "score" in result_metrics
101+
assert (ctx_metrics.run_dir / "artifacts" / "return_metrics.json").exists()
102+
finally:
103+
_close_ctx_logger(ctx_metrics)
104+
105+
ctx_stream = _make_ctx(tmp_path, {"scenario": "stream_only", "lr": 0.005})
106+
try:
107+
result_stream = module.exp_fn(ctx_stream)
108+
assert result_stream is None
109+
assert (ctx_stream.run_dir / "artifacts" / "stream_only.json").exists()
110+
rows = utils.load_jsonl(ctx_stream.run_dir / "metrics.jsonl", skip_invalid=True)
111+
assert len(rows) >= 2
112+
finally:
113+
_close_ctx_logger(ctx_stream)
114+
115+
ctx_skip = _make_ctx(tmp_path, {"scenario": "skip", "lr": 0.01})
116+
try:
117+
with pytest.raises(SkipRun):
118+
module.exp_fn(ctx_skip)
119+
assert (ctx_skip.run_dir / "artifacts" / "skip.json").exists()
120+
finally:
121+
_close_ctx_logger(ctx_skip)
122+
123+
ctx_fail = _make_ctx(tmp_path, {"scenario": "fail", "lr": 0.02})
124+
try:
125+
with pytest.raises(RuntimeError):
126+
module.exp_fn(ctx_fail)
127+
assert (ctx_fail.run_dir / "artifacts" / "fail.json").exists()
128+
finally:
129+
_close_ctx_logger(ctx_fail)

0 commit comments

Comments
 (0)