220 lines
13 KiB
Markdown
220 lines
13 KiB
Markdown
# 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 的元数据文件,让每次训练都能绑定代码版本和数据版本。
|
||
|
||
这两步成本不高,但会让后面的模型调优、实验对比和远程训练结果变得可信。
|
||
|