Files
lemdb/docs/development-report.md

220 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <sqlite_path> <artifact.joblib>
```
命令会自动生成同名指标报告,例如 `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 的元数据文件,让每次训练都能绑定代码版本和数据版本。
这两步成本不高,但会让后面的模型调优、实验对比和远程训练结果变得可信。