新增 core/model-framework/feature_spec.py:把超参包中特征 spec 字段从 「非空字符串存在性校验」(issue #39)升级为可解释的声明式特征定义引擎。 - 手写递归下降解析器(tokenizer+parser),安全无 eval/exec;支持算子调用、 裸点位、数值/窗口字面量、嵌套、中文点位、带符号/科学计数数值。 - 不可变 AST(TagRef/Number/Window/OpCall),to_dict/repr 可序列化往返。 - 语义校验:未知算子、arity、参数 kind;返回 SpecIssue 列表供配置台聚合展示。 - 依赖分析 resolve_inputs:去重、稳定顺序,供训练/推理流水线按 tag 拉取数据。 - materialize 执行:EMA/SMA/RollingStd/Max/Min/RateOfChange/Diff/Lag/Log/ Scale/Clip/Combine 共 12 个内置算子,缺数据 fail-fast;numpy 可选依赖。 - 插件注册 register_operator:对齐 PRD「新增结构走插件注册」(呼应 #34 Recipe)。 - 49 个单元测试全通过;零内核改动,纯新增。
3.4 KiB
3.4 KiB
iAOP-Core · FeatureSpec 声明式特征定义引擎
对应 issue #35(父 EPIC #5「③ AI 模型框架 配置化重构」)与 PRD 5.3「超参包驱动」:
超参包中每个特征的 spec 字段是一段声明式特征定义表达式(FeatureSpec),本
模块负责把这段文本解释为可执行的特征计算:
EMA(CLF-01.TEMP, 5m)→ 解析为 AST → 校验 → 依赖分析 → materialize 为时序
core/model-framework/hyperparam.py(issue #39)只对 spec 做「非空字符串」存在性
校验;本模块补齐其语义层,使:
- 配置台(issue #62~#67 Template Console)导入超参包时即可一次性展示每个特征 的解析结果与依赖点位,训练前暴露拼写/语义错误;
- 训练/推理流水线(issue #40)拿到 AST 后直接 materialize 为「按点位拉取 → 算子 计算」的特征管道;
- 切换模板仅改超参包,特征逻辑零代码(对齐 PRD「配置化」核心目标)。
模块结构
core/model-framework/
├── feature_spec.py FeatureSpec 引擎(解析/校验/依赖/执行/算子注册)
└── tests/
├── _bootstrap.py 测试引导(目录含连字符,挂载包名 model_framework)
└── test_feature_spec.py 解析/校验/依赖/执行/插件注册单元测试
设计要点
- 零外部强依赖:解析/校验/依赖分析不依赖第三方库;执行(
materialize)优先用 numpy 向量化,无 numpy 时退化为纯 Python(边缘/离线环境可加载与校验)。 - 不可变 AST + 函数式算子:每个算子是纯函数
op(series_map, args),注册到OPERATORS;新增算子只需register_operator(对齐 PRD「新增结构走插件注册」, 与 issue #34 Model Recipe 插件接口呼应)。 - 安全解析:手写递归下降解析器,绝不使用
eval/exec——FeatureSpec 是数据 而非代码,避免任意表达式注入。
语法(对齐 PRD 5.3 示例)
<Operator>(<arg>, <arg>, ...) # 一元/多元算子
<arg> := <tag> | <number> | <window> | <Operator>(...)
<tag> := 点位名,允许中文/点号/连字符,如 CLF-01.TEMP / 炉压
<number> := 整数或浮点(含负号),如 3、-0.5、1e-3
<window> := <正数><单位>,单位 d/h/m/s,如 5m、180d、10s
内置算子:EMA / SMA / RollingStd / RollingMax / RollingMin / RateOfChange / Diff / Lag / Log / Scale / Clip / Combine,覆盖 PRD 5.3 超参包示例的全部特征形态。
使用
from model_framework.feature_spec import (
parse, validate, resolve_inputs, materialize, describe, register_operator,
)
ast = parse("EMA(CLF-01.TEMP, 5m)") # 1. 解析
issues = validate(ast) # 2. 语义校验(空列表=通过)
tags = resolve_inputs(ast) # 3. 依赖点位:['CLF-01.TEMP']
feat = materialize(ast, {"CLF-01.TEMP": xs}) # 4. 执行 → list[float]
print(describe(ast)) # 人类可读描述
扩展算子(插件注册):
register_operator(
"Double", min_arity=1, max_arity=1, arg_kinds=(("tag",),),
func=lambda s, a: [x * 2 for x in s[a[0].name]], doc="翻倍",
)
materialize(parse("Double(A)"), {"A": [1.0, 2.0]}) # → [2.0, 4.0]
测试
cd core/model-framework/tests
python -m unittest test_feature_spec