Files
AlphaMaster/prune_features.py
T
2026-07-04 10:34:51 +08:00

140 lines
6.2 KiB
Python
Raw 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.
"""
prune_features.py — 特征剪枝:从 65 个特征中筛出有区分度、低冗余的子集
背景:
特征库扩展到 65 个后,vocab=131,8-token 搜索空间暴增到 ~8.67×10^16。
其中大量特征高度相关(多周期均线/动量/通道位置类),是噪声维度。
做法(用 model_core.evaluator):
1. 离线加载全部品种数据,直接从 _FEATURE_DEFS 计算全部 65 个特征
2. score_all:对每个特征算 IC / RankIC / 互信息,跨特征秩归一聚合 importance
3. prune:保守双条件相关性剪枝(corr>阈值 且 分差>margin 才剪)
4. 额外按 importance 取 Top-K,去掉近零 IC 的弱特征
5. 写出 active_features.json(features.py 启动时读取,只注册白名单特征)
用法:
python prune_features.py # 默认 corr_threshold=0.85, top_k=28
python prune_features.py --top-k 25 # 自定义保留数量
python prune_features.py --corr 0.9 # 自定义相关性阈值
"""
import argparse
import json
import sys
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).parent))
from config import Config
from data_pipeline.data_manager import MT5DataManager
from data_pipeline.fetcher import MT5DataFetcher
from model_core.features import _FEATURE_DEFS
from model_core.evaluator import score_all, prune
OUTPUT = Path(__file__).parent / "active_features.json"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--top-k", type=int, default=28,
help="剪枝后额外按 importance 截断保留的最大特征数")
ap.add_argument("--corr", type=float, default=0.85,
help="相关性剪枝阈值(越低剪得越狠)")
ap.add_argument("--margin", type=float, default=0.01,
help="剪枝分差阈值(势均力敌时保留)")
args = ap.parse_args()
print(f"{'='*62}")
print(f" 特征剪枝 corr_threshold={args.corr} top_k={args.top_k}")
print(f"{'='*62}")
# ── 1. 离线加载数据 ───────────────────────────────────────────────
print("加载数据(离线缓存)...")
with MT5DataFetcher(offline=True) as fetcher:
mgr = MT5DataManager(fetcher)
mgr.load()
raw_dict = mgr.raw_dict
target = mgr.target_ret # [N, T]
syms = mgr.symbols
N, T = target.shape
print(f" 品种={syms} N={N} T={T}")
# ── 2. 直接从 _FEATURE_DEFS 计算全部 65 个特征(绕过白名单)───────
print(f"\n计算全部 {len(_FEATURE_DEFS)} 个特征...")
candidates: dict[str, torch.Tensor] = {}
categories: dict[str, str] = {}
for name, category, compute in _FEATURE_DEFS:
try:
series = compute(raw_dict) # [N, T]
candidates[name] = torch.nan_to_num(series, nan=0.0,
posinf=0.0, neginf=0.0)
categories[name] = category
except Exception as e:
print(f" [跳过] {name}: 计算失败 {e}")
# ── 3. 打分 ───────────────────────────────────────────────────────
print(f"\n对 {len(candidates)} 个特征打分(IC / RankIC / MI)...")
scores = score_all(candidates, target, categories=categories)
scores_sorted = sorted(scores, key=lambda s: (
-s.importance_score if s.importance_score == s.importance_score else 1,
))
print(f"\n {'特征':22s}{'类别':14s}{'IC':>8}{'RankIC':>8}{'MI':>8}{'importance':>12}")
print(f" {'-'*70}")
for s in scores_sorted:
imp = s.importance_score
imp_str = f"{imp:.4f}" if imp == imp and imp > -1e30 else " 退化"
print(f" {s.candidate:22s}{s.category:14s}"
f"{s.ic:+8.4f}{s.rank_ic:+8.4f}{s.mi:8.4f}{imp_str:>12}")
# ── 4. 相关性剪枝 ─────────────────────────────────────────────────
print(f"\n相关性剪枝(corr>{args.corr} 且分差>{args.margin})...")
rows = prune(scores, candidates, corr_threshold=args.corr, margin=args.margin)
retained = [r for r in rows if r.retention_status == "retained"]
pruned = [r for r in rows if r.retention_status == "pruned"]
print(f" 相关性剪枝后保留 {len(retained)} 个,剪掉 {len(pruned)} 个")
for r in pruned:
print(f" ✗ {r.candidate:22s} (favor of {r.pruned_in_favor_of})")
# ── 5. 按 importance 取 Top-K(去掉弱特征)────────────────────────
retained_sorted = sorted(
retained,
key=lambda r: (r.importance_score if r.importance_score == r.importance_score
and r.importance_score > -1e30 else -1e30),
reverse=True,
)
final = retained_sorted[:args.top_k]
final_names_set = {r.candidate for r in final}
# 保持 _FEATURE_DEFS 原始顺序输出
ordered_names = [name for name, _, _ in _FEATURE_DEFS if name in final_names_set]
print(f"\n{'='*62}")
print(f" 最终保留 {len(ordered_names)} 个特征(vocab 从 65 特征降至 {len(ordered_names)})")
print(f"{'='*62}")
for n in ordered_names:
print(f" ✓ {n}")
# ── 6. 写出 active_features.json ─────────────────────────────────
payload = {
"active_features": ordered_names,
"meta": {
"source": "prune_features.py",
"corr_threshold": args.corr,
"top_k": args.top_k,
"n_original": len(_FEATURE_DEFS),
"n_retained": len(ordered_names),
"symbols": syms,
"bars": T,
},
}
OUTPUT.write_text(json.dumps(payload, indent=2, ensure_ascii=False),
encoding="utf-8")
print(f"\n已写出 → {OUTPUT}")
print("重新 import model_core.features 时将只注册白名单特征。\n")
if __name__ == "__main__":
main()