From 1b30fddfbf2bb2070de6a8a5fe6935ed7ef2dd8e Mon Sep 17 00:00:00 2001 From: obroccolio Date: Fri, 11 Sep 2026 17:24:44 +0800 Subject: [PATCH] docs: add development progress report --- docs/development-report.md | 219 +++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 docs/development-report.md diff --git a/docs/development-report.md b/docs/development-report.md new file mode 100644 index 0000000..927157e --- /dev/null +++ b/docs/development-report.md @@ -0,0 +1,219 @@ +# LMPM 当前进展与下一步路线 + +> 报告基线:`main @ 8e0b371 feat: add routed model training pipeline` +> 验证结果:`uv run pytest -q` 通过 25 个测试;`uv run ruff check .` 与 `uv run ruff format --check .` 全部通过。 + +## 1. 一句话总结 + +当前仓库已经从“架构文档 + 领域模型雏形”推进到一条可复现的模型训练底座:外部数据平台中的 `material_records` 表可以按固定契约被只读加载,特征可以统一填补、缩放和编码,六个实验结果会按分类/回归分别路由到默认模型,训练完成后按材料做无泄漏 LOMO 验证,并把模型与验证报告一起落盘。 + +目前最关键的进展不是某个模型精度,而是把“外部平台数据 → 规范化训练数据 → 特征处理 → 多目标模型 → 评估报告 → 可复用产物”这条链路用代码固化下来了。后面接入真实数据时,工程路径不需要重写。 + +## 2. 当前已经完成的能力 + +### 2.1 仓库与工程基础 + +仓库已经建立 `src` 布局、`uv` 依赖管理、`pytest` 测试和 `Ruff` 静态检查。核心配置集中在 `pyproject.toml`,锁文件 `uv.lock` 保证了本地、Ubuntu 和后续 Docker 环境使用同一套依赖版本。 + +当前 Python 代码约 927 行,测试代码约 572 行。测试覆盖了领域校验、SQLite 往返、外部数据平台读取、目标拆分、特征预处理、模型路由、模型训练、评估和产物恢复。 + +### 2.2 领域模型与本地存储骨架 + +`src/lmpm/domain/material.py` 定义了 `MaterialProperty`,用于约束材料名称、类别、反射率、吸收率、熔点、热导率、密度、粗糙度等基础物性。 + +`src/lmpm/domain/experiment.py` 定义了 `ProcessingParams`、`QualityMetrics` 和 `ExperimentRecord`,把功率、扫描速度、脉冲频率等加工参数与切口宽度、热影响区、毛刺高度等质量指标分开建模。 + +`src/lmpm/data/store.py` 提供了一个最小 SQLite 本地骨架,支持 `materials` 与 `experiments` 表的初始化和读写。它的价值是先稳定领域对象的持久化方式,但它不是训练数据的最终来源。 + +### 2.3 外部数据平台的只读训练入口 + +`src/lmpm/training/dataset.py` 现在是训练侧最关键的数据入口。它从外部数据平台的 SQLite 文件读取 `material_records`,并固定了当前训练契约: + +| 数据块 | 字段数量 | 说明 | +| --- | ---: | --- | +| metadata | 4 | 实验 ID、测试日期、材料类别、材料名称 | +| numeric features | 18 | 材料物性、激光参数等连续特征 | +| categorical features | 2 | 填充方式、加工尺寸 | +| targets | 6 | 布尔结果 2 个,连续结果 4 个 | + +`TrainingDataset` 和 `TrainingRecord` 使用 Pydantic 定义,记录对象是不可变的。加载器使用 SQLite 的 `mode=ro` 只读模式,明确表达“训练数据来自外部平台,本仓库不负责写入或修改源数据”。如果缺少 `material_records` 表或缺少必需字段,代码会抛出明确错误,而不是静默生成残缺数据集。 + +### 2.4 目标字段的任务拆分 + +`src/lmpm/training/targets.py` 把六个结果字段明确分成两类: + +| 目标字段 | 任务类型 | +| --- | --- | +| `is_cut_through` | classification | +| `is_fire_smolder` | classification | +| `carbonized_edge_width` | regression | +| `etching_depth` | regression | +| `pattern_clarity_score` | regression | +| `presentation_balance_score` | regression | + +这解决了一个很容易出错的问题:六个实验结果不是同一种学习任务。不能把布尔结果和连续结果丢进同一个回归器,也不能用同一套指标评估所有目标。现在每个目标都有稳定的任务类型契约,缺失目标会带实验 ID 报错,便于定位到具体实验。 + +### 2.5 特征预处理策略 + +`src/lmpm/training/preprocessing.py` 建立了统一的 `FeaturePreprocessor`: + +| 特征类型 | 缺失值策略 | 后续处理 | +| --- | --- | --- | +| numeric features | median | `StandardScaler` | +| categorical features | most frequent | one-hot encoding | + +数值特征使用中位数填补,比均值对异常值更稳健。类别特征使用训练集中最常见的取值填补,然后做 one-hot 编码。`OneHotEncoder` 使用 `handle_unknown="ignore"`,推理阶段遇到训练时没见过的类别会编码为全零,而不是直接让服务崩溃。 + +预处理器的输出是 `PreprocessedFeatures`,同时携带特征矩阵、展开后的特征名和实验 ID。这样后续模型评估既能拿到数值矩阵,也能保留可追踪的数据来源。 + +### 2.6 模型路由与基线训练 + +`src/lmpm/training/routing.py` 定义了默认模型路由: + +| 任务类型 | 默认模型 | +| --- | --- | +| regression | GaussianProcessRegressor | +| classification | RandomForestClassifier | + +`src/lmpm/training/models.py` 中,回归使用 `RBF + WhiteKernel` 的 GPR,并开启 `normalize_y=True`。分类使用 300 棵树的随机森林,并使用 `class_weight="balanced"`。这是一个刻意保守的小样本基线:GPR 天然输出均值和不确定性,适合连续物理量;随机森林对布尔结果、类别交互和小数据更稳健。 + +训练流程采用按材料名称分组的 Leave-One-Group-Out。也就是说,验证时不是把同一块材料的两条实验随机分到训练集和测试集,而是把一整块材料作为 held-out group。这更接近真实使用场景:模型需要面对没见过的材料,而不是插值同一材料内部的小变化。 + +### 2.7 验证、评估与产物落盘 + +每个目标都会独立训练和评估。回归目标输出 MAE、RMSE、R2;分类目标输出 accuracy、F1 和 ROC AUC。ROC AUC 只有在当前验证折同时包含正负类时才会输出,否则为 `null`,避免伪造指标。 + +最终产物分两类: + +1. `*.joblib`:包含每个目标的 `TargetModelRoute`、`FeaturePreprocessor` 和 estimator。 +2. `*.metrics.json`:包含目标级任务类型、模型类型、平均指标和每个 held-out material 的折内指标。 + +`src/lmpm/training/workflow.py` 提供了一步式入口 `train_sqlite_database()`。`src/lmpm/scripts/train.py` 进一步暴露为 CLI: + +```bash +uv run python -m lmpm.scripts.train +``` + +命令会自动生成同名指标报告,例如 `artifact.metrics.json`。 + +## 3. 为什么采用当前设计 + +### 3.1 为什么把外部平台作为唯一训练数据源 + +如果项目内部再复制一份数据存储,很快会出现“哪个库是权威版本”的问题。当前设计把 `material_records` 当作唯一事实源,仓库只负责读取、校验、建模和交付。这样可以避免训练数据和实验记录分叉,也方便后续通过接口或数据库快照直接接入平台。 + +### 3.2 为什么先用 SQLite,而不是立刻做 HTTP 接口 + +外部平台已经有 SQLite 数据形态,而且备份文件可以直接本地验证。先用 SQLite 把字段契约、缺失值、目标类型、模型输入输出打通,是最小风险路径。等平台开始正式提供接口后,可以在 `training/` 层增加一个同样输出 `TrainingDataset` 的 HTTP 适配器,不需要改动下游模型代码。 + +### 3.3 为什么按目标独立建模 + +六个结果字段之间的类型、物理含义和量纲都不同。`is_cut_through` 是布尔结果,`carbonized_edge_width` 是长度量,`pattern_clarity_score` 是评价分数。强行使用一个多输出模型会让标签缺失、指标解释和不确定性输出都变复杂。当前采用“一个目标一个模型”的方式,虽然产物文件更大,但行为更清楚,也更容易按目标替换模型。 + +### 3.4 为什么先做 GPR 和随机森林 + +架构报告中的长期方案是 GPR、XGBoost、随机森林等多模型对比。当前实现先落到两个稳定基线: + +- GPR 适合小样本连续回归,并能自然给出不确定性。 +- RandomForestClassifier 对布尔分类和小数据集稳健,也便于后续解释特征重要性。 + +这保证系统先有可验证闭环,再进入大规模调参和模型对比。 + +### 3.5 为什么按材料做 LOMO + +随机按行交叉验证会低估真实部署风险,因为同一材料的相邻实验往往高度相似。按 `material_name` 做 LOMO 更接近“模型没见过这个材料”的场景,是跨材料泛化能力的更诚实的评估方式。 + +## 4. 与之前状态相比的提升 + +| 维度 | 之前状态 | 当前状态 | +| --- | --- | --- | +| 数据来源 | 主要依赖架构文档描述 | 外部 SQLite `material_records` 可以被只读加载 | +| 训练契约 | 字段和目标没有完全落地 | metadata、features、targets 数量与字段固定 | +| 目标处理 | 六个结果没有按学习任务拆开 | 2 个分类、4 个回归显式路由 | +| 特征处理 | 只有概念设计 | 缺失填补、标准化、one-hot、未知类别处理已实现 | +| 模型训练 | 只有技术选型 | 每个目标有默认 estimator 并可训练 | +| 验证方式 | 计划做 LOMO | 已按 material_name 做无泄漏 LOMO | +| 模型交付 | 无产物方案 | joblib 模型 + JSON 指标报告 | +| 工程验证 | 缺少可执行测试 | 25 个测试通过,Ruff 全绿 | + +最重要的变化是:当前仓库不再只是“计划中的架构”,而是一个可以被命令驱动的训练原型。 + +## 5. 当前限制 + +### 5.1 真实数据规模仍然不足 + +当前测试使用的是构造的 SQLite schema 和样本记录。虽然外部数据库入口已经被验证,但还不能证明在真实数据分布上有稳定精度。GPR 和随机森林的默认参数也只是基线,尚未调优。 + +### 5.2 特征工程还很浅 + +目前只是把原始字段填补、缩放和编码,还没有做物理特征组合,例如能量密度、热影响比、材料厚度归一化后的加工强度等。这些组合特征可能会显著影响跨材料泛化。 + +### 5.3 预处理与模型都还没有版本化元数据 + +joblib 文件目前能保存模型和预处理器,但还没有把训练数据哈希、代码版本、数据库快照标识、训练时间、字段顺序等完整元数据写进 artifact。等真实数据接入后,这会是复现实验的必要条件。 + +### 5.4 GPR 的 ONNX 导出仍是风险项 + +架构报告已经把 GPR 的 ONNX 精度问题列为受控风险。当前仓库使用 joblib 落盘,暂时绕开了这个问题。后续如果要进入服务化部署,需要专门做 sklearn 原生预测和 ONNX Runtime 预测的精度对比。 + +## 6. 下一步路线 + +### 第一步:确认真实数据形态 + +优先做数据画像,而不是继续扩展模型。需要确认: + +- `material_records` 实际有多少行。 +- 每个特征列的缺失率。 +- 每个目标的分布和类别比例。 +- 有多少个不同 `material_name`。 +- 每个材料下有多少组实验。 +- 激光参数是否覆盖足够广的参数空间。 + +这个阶段的产物应该是一份自动生成或半自动生成的数据画像报告。 + +### 第二步:抽象数据访问层 + +现在已经有 SQLite 路径。下一步可以引入一个简单的 repository 接口,例如 `SqliteTrainingRepository` 和未来的 `HttpTrainingRepository`,两者都返回 `TrainingDataset`。这样平台切换或接口接入不会影响训练代码。 + +### 第三步:补齐数据校验和指标报告 + +在训练前增加更严格的数据画像检查: + +- 空数据集、单材料、单类别、全缺失列的提示。 +- 目标分布报告。 +- 特征覆盖率报告。 +- 每个材料组的样本数量统计。 + +这样模型精度低时,可以更快判断是数据问题、特征问题还是模型问题。 + +### 第四步:引入模型版本化 + +joblib 产物旁边应该增加一个训练元数据文件,至少记录: + +- 代码 commit。 +- 数据文件 SHA-256。 +- schema/feature 版本。 +- 目标字段顺序。 +- 训练时间。 +- LOMO 指标。 +- 模型参数。 + +这一步能让后续实验结果真正可比。 + +### 第五步:做多模型对比 + +真实数据到齐后,再引入 XGBoost、更充分的随机森林对比,以及架构报告中规划的 Conformal Prediction 层。对比时保持统一的特征处理、统一的 LOMO 分组和统一指标。 + +### 第六步:推理与部署 + +训练底座稳定后,增加 `predict` 入口和 FastAPI 推理服务。部署继续按当前约定走 Ubuntu + Docker,依赖以 `uv.lock` 为准,避免在服务器上手工编译科学计算依赖。 + +## 7. 立即建议 + +下一轮不建议先写更复杂的模型,而是先做两件小事: + +1. 写一个数据画像脚本,读取外部 SQLite,输出目标分布、缺失率和材料分组统计。 +2. 引入训练 artifact 的元数据文件,让每次训练都能绑定代码版本和数据版本。 + +这两步成本不高,但会让后面的模型调优、实验对比和远程训练结果变得可信。 +