feat(#42): 模型框架模板化PoC(真实数据验证·降RISK,Ti+树脂场景端到端验证R1精度/R2配方切换/R3阶段回滚)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""测试引导:把连字符目录 ``core/model-framework`` 加载为可导入包
|
||||
``model_framework``,使测试可 ``from model_framework import ...``。
|
||||
"""
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
PKG_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def _load_package(name: str, path: str) -> None:
|
||||
if name in sys.modules:
|
||||
return
|
||||
init_py = os.path.join(path, "__init__.py")
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
name, init_py, submodule_search_locations=[path])
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
|
||||
_load_package("model_framework", PKG_DIR)
|
||||
@@ -0,0 +1,170 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""模型框架模板化 PoC 单元测试(issue #42)。
|
||||
|
||||
覆盖:
|
||||
- 轻量主干(_LinearBackbone / _MeanBackbone)训练预测 + _solve_linear 正确性;
|
||||
- _MiniRegistry 注册 / promote / rollback / serving;
|
||||
- PoCScenario 构造 + _gen_linear_samples 确定性;
|
||||
- TemplatePoC.run 端到端链路 + PoCReport 三条 RISK 验收口径(R1/R2/R3);
|
||||
- 内置 Ti + 树脂场景跑通且 all_passed。
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
if HERE not in sys.path:
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
import _bootstrap # noqa: E402 加载 model_framework 包
|
||||
|
||||
from model_framework import ( # noqa: E402
|
||||
PoCError,
|
||||
PoCReport,
|
||||
PoCScenario,
|
||||
TemplatePoC,
|
||||
resin_quality_scenario,
|
||||
run_poc,
|
||||
ti_quality_scenario,
|
||||
)
|
||||
from model_framework.template_poc import ( # noqa: E402
|
||||
BACKBONES,
|
||||
_Artifact,
|
||||
_gen_linear_samples,
|
||||
_LinearBackbone,
|
||||
_MeanBackbone,
|
||||
_MiniRegistry,
|
||||
_solve_linear,
|
||||
)
|
||||
|
||||
|
||||
class TestSolveLinear(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
# 2x + 3y = 8; x - y = 1 => x=2.2, y=1.2
|
||||
w = _solve_linear([[2, 3], [1, -1]], [8, 1])
|
||||
self.assertAlmostEqual(w[0], 2.2, places=6)
|
||||
self.assertAlmostEqual(w[1], 1.2, places=6)
|
||||
|
||||
def test_identity(self):
|
||||
w = _solve_linear([[1, 0], [0, 1]], [3, 5])
|
||||
self.assertEqual(w, [3.0, 5.0])
|
||||
|
||||
|
||||
class TestBackbones(unittest.TestCase):
|
||||
def test_linear_fits_linear_data(self):
|
||||
# y = 1 + 2*x1 + 3*x2
|
||||
X = [[0, 0], [1, 0], [0, 1], [1, 1], [2, 3]]
|
||||
y = [1 + 2 * x1 + 3 * x2 for x1, x2 in X]
|
||||
bb = _LinearBackbone({"lambda": 0.0})
|
||||
bb.fit(X, y)
|
||||
preds = bb.predict([[1, 1], [2, 2]])
|
||||
self.assertAlmostEqual(preds[0], 6.0, places=4)
|
||||
self.assertAlmostEqual(preds[1], 11.0, places=4)
|
||||
|
||||
def test_mean_backbone(self):
|
||||
bb = _MeanBackbone({})
|
||||
bb.fit([[1], [2], [3]], [10, 20, 30])
|
||||
self.assertEqual(bb.predict([[9]]), [20.0])
|
||||
|
||||
def test_backbones_registered(self):
|
||||
self.assertIn("linear", BACKBONES)
|
||||
self.assertIn("mean", BACKBONES)
|
||||
|
||||
|
||||
class TestMiniRegistry(unittest.TestCase):
|
||||
def test_register_promote_rollback(self):
|
||||
reg = _MiniRegistry()
|
||||
reg.register(_Artifact("m", "v1", "linear", {"mae": 1.0}))
|
||||
self.assertEqual(reg.serving("m", "dev"), "v1")
|
||||
reg.promote("m", "v1") # dev->staging
|
||||
reg.promote("m", "v1") # staging->prod
|
||||
self.assertEqual(reg.serving("m", "prod"), "v1")
|
||||
reg.register(_Artifact("m", "v2", "linear", {"mae": 0.8}))
|
||||
reg.promote("m", "v2")
|
||||
reg.promote("m", "v2")
|
||||
reg.rollback("m", "prod", "v1")
|
||||
self.assertEqual(reg.serving("m", "prod"), "v1")
|
||||
|
||||
def test_promote_prod_raises(self):
|
||||
reg = _MiniRegistry()
|
||||
reg.register(_Artifact("m", "v1", "linear", {}))
|
||||
reg.promote("m", "v1")
|
||||
reg.promote("m", "v1")
|
||||
with self.assertRaises(PoCError):
|
||||
reg.promote("m", "v1")
|
||||
|
||||
|
||||
class TestGenSamples(unittest.TestCase):
|
||||
def test_deterministic(self):
|
||||
s1 = _gen_linear_samples(10, 3, seed=42)
|
||||
s2 = _gen_linear_samples(10, 3, seed=42)
|
||||
self.assertEqual(s1, s2)
|
||||
|
||||
def test_shape(self):
|
||||
s = _gen_linear_samples(20, 4, seed=1)
|
||||
self.assertEqual(len(s), 20)
|
||||
self.assertEqual(len(s[0]), 5) # 4 feat + 1 target
|
||||
|
||||
|
||||
class TestTemplatePoC(unittest.TestCase):
|
||||
def test_run_two_scenarios_all_passed(self):
|
||||
report = run_poc()
|
||||
self.assertIsInstance(report, PoCReport)
|
||||
self.assertEqual(len(report.scenario_results), 2)
|
||||
self.assertTrue(report.all_passed, report.summary())
|
||||
self.assertTrue(report.r1_precision_ok)
|
||||
self.assertTrue(report.r2_recipe_switch_ok)
|
||||
self.assertTrue(report.r3_stage_rollback_ok)
|
||||
|
||||
def test_r1_precision_fails_on_bad_acceptance(self):
|
||||
# 把验收线设极小,强制 R1 失败
|
||||
sc = ti_quality_scenario()
|
||||
sc.acceptance_mae = 0.0001 # 不可能达到
|
||||
report = TemplatePoC([sc]).run()
|
||||
self.assertFalse(report.r1_precision_ok)
|
||||
|
||||
def test_r2_recipe_switch_detects_mixed_backbone(self):
|
||||
sc1 = ti_quality_scenario()
|
||||
sc2 = resin_quality_scenario()
|
||||
sc2.backbone = "mean" # 故意用不同主干
|
||||
report = TemplatePoC([sc1, sc2]).run()
|
||||
self.assertFalse(report.r2_recipe_switch_ok)
|
||||
|
||||
def test_r3_rollback_serving_correct(self):
|
||||
report = run_poc()
|
||||
for sr in report.scenario_results:
|
||||
self.assertTrue(sr["serving_is_v1"])
|
||||
|
||||
def test_to_dict_serializable(self):
|
||||
import json
|
||||
report = run_poc()
|
||||
d = report.to_dict()
|
||||
json.dumps(d) # 可序列化
|
||||
self.assertIn("R1_precision_ok", d)
|
||||
|
||||
def test_insufficient_samples_raises(self):
|
||||
sc = PoCScenario(name="x", industry="t",
|
||||
feature_columns=("a",), target_column="y",
|
||||
samples=[[1, 2]]) # 不足
|
||||
with self.assertRaises(PoCError):
|
||||
TemplatePoC([sc]).run()
|
||||
|
||||
def test_unknown_backbone_raises(self):
|
||||
sc = PoCScenario(name="x", industry="t",
|
||||
feature_columns=("a",), target_column="y",
|
||||
backbone="voodoo",
|
||||
samples=_gen_linear_samples(20, 1, seed=1))
|
||||
with self.assertRaises(PoCError):
|
||||
TemplatePoC([sc]).run()
|
||||
|
||||
def test_builtin_scenarios_distinct(self):
|
||||
ti = ti_quality_scenario()
|
||||
resin = resin_quality_scenario()
|
||||
self.assertNotEqual(ti.feature_columns, resin.feature_columns)
|
||||
self.assertEqual(ti.backbone, resin.backbone) # 共用主干(R2)
|
||||
self.assertNotEqual(ti.hyperparams, resin.hyperparams) # 配方不同
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user