Files
lemdb/docs/development-report.md

13 KiB
Raw Permalink Blame History

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 定义了 ProcessingParamsQualityMetricsExperimentRecord,把功率、扫描速度、脉冲频率等加工参数与切口宽度、热影响区、毛刺高度等质量指标分开建模。

src/lmpm/data/store.py 提供了一个最小 SQLite 本地骨架,支持 materialsexperiments 表的初始化和读写。它的价值是先稳定领域对象的持久化方式,但它不是训练数据的最终来源。

2.3 外部数据平台的只读训练入口

src/lmpm/training/dataset.py 现在是训练侧最关键的数据入口。它从外部数据平台的 SQLite 文件读取 material_records,并固定了当前训练契约:

数据块 字段数量 说明
metadata 4 实验 ID、测试日期、材料类别、材料名称
numeric features 18 材料物性、激光参数等连续特征
categorical features 2 填充方式、加工尺寸
targets 6 布尔结果 2 个,连续结果 4 个

TrainingDatasetTrainingRecord 使用 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:包含每个目标的 TargetModelRouteFeaturePreprocessor 和 estimator。
  2. *.metrics.json:包含目标级任务类型、模型类型、平均指标和每个 held-out material 的折内指标。

src/lmpm/training/workflow.py 提供了一步式入口 train_sqlite_database()src/lmpm/scripts/train.py 进一步暴露为 CLI

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 的元数据文件,让每次训练都能绑定代码版本和数据版本。

这两步成本不高,但会让后面的模型调优、实验对比和远程训练结果变得可信。