Download causal_tech_stack_demo.py from logos42/headless: direct link, hf CLI and curl.
- Browser
- Download file 8.8 kB
-
https://huggingface.co/logos42/headless/resolve/main/causal_tech_stack_demo.py
- Command line
-
hf download hf://logos42/headless/causal_tech_stack_demo.py
-
curl -L -o causal_tech_stack_demo.py https://huggingface.co/logos42/headless/resolve/main/causal_tech_stack_demo.py
8.8 kB
| #!/usr/bin/env python3 | |
| """headless · Causal AI 技术栈端到端演示 | |
| 把《Causal AI》的因果技术落地到 headless 的 S4 元学习世界,并展示 | |
| "用因果发现改进自主数据提议器"的全链路: | |
| STEP 1 把 S4 世界建模为 SCM(生成元 -> 循环结构 -> 泛化表现) | |
| STEP 2 d-separation:识别因果路径的独立性 | |
| STEP 3 do() 干预:覆盖生成元的因果效应 | |
| STEP 4 因果发现:从观测 acc 反估计"生成元 -> 泛化"的依赖 | |
| STEP 5 识别:后门调整集 | |
| STEP 6 反事实:Pearl 三步(Abduction / Action / Prediction) | |
| STEP 7 因果提议器 vs 启发式提议器(自主探索效率对比)★ | |
| STEP 8 Causal RL:状态/动作/奖励 + 因果 credit assignment | |
| 运行:python3 causal_tech_stack_demo.py (纯 numpy,秒级) | |
| """ | |
| import os | |
| import sys | |
| ROOT = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, ROOT) | |
| from hibs_lnn.causal import ( # noqa: E402 | |
| S4WorldSCM, Graph, d_separated, | |
| backdoor_adjustment_set, g_formula, | |
| CausalDiscovery, CausalProposer, CausalRLAgent, | |
| ) | |
| def show(title): | |
| print("\n" + "=" * 72) | |
| print(title) | |
| print("=" * 72) | |
| def main(): | |
| s = S4WorldSCM(seed=0) | |
| # ---- STEP 1: SCM ---- | |
| show("STEP 1 把 S4 元学习世界建模为结构因果模型 (SCM)") | |
| print(" 结构方程 (PARENTS):") | |
| for v, pa in S4WorldSCM.PARENTS.items(): | |
| print(" %-5s <- %s" % (v, ", ".join(pa))) | |
| print(" 变量:G 生成元集合(因) / cyc 循环类型 / obs 观测特征 / acc 泛化(果)") | |
| p4 = (1, 2, 3, 0) | |
| print("\n 例 perm=(1,2,3,0): cyc=%d, 生成元签名=%s" | |
| % (s.cyc(p4), s.sig_bits(p4))) | |
| print(" acc(未覆盖生成元)=%.3f, do(覆盖其生成元)=%.3f" | |
| % (s.acc(p4, set(), noise=0.0), s.do_intervene(p4, set()))) | |
| # ---- STEP 2: d-separation ---- | |
| show("STEP 2 d-separation —— 因果图中路径的独立性") | |
| Gc = Graph({'cyc': {'G'}, 'obs': {'G', 'cyc'}, 'acc': {'obs', 'cyc'}}) | |
| print(" 图: G->cyc->acc 链 + G->obs、cyc->obs、obs->acc(obs 是另一中间变量)") | |
| print(" 仅条件 {cyc} 不够:还存在 G->obs->acc 链,需同时阻断 obs") | |
| print(" G 与 acc 无条件相连接 ->", | |
| d_separated(Gc, {'G'}, {'acc'}, set())) | |
| print(" G ┴ acc | {cyc, obs} ->", | |
| d_separated(Gc, {'G'}, {'acc'}, {'cyc', 'obs'})) | |
| gc = Graph({'cyc': {'G'}, 'acc': {'cyc'}}) | |
| print(" 最短链 G->cyc->acc |{cyc}->", | |
| d_separated(gc, {'G'}, {'acc'}, {'cyc'})) | |
| # ---- STEP 3: do() 干预 ---- | |
| show("STEP 3 do() 干预 —— 覆盖生成元对泛化的因果效应") | |
| learned0 = [(0, 1, 2, 3), (1, 0, 2, 3)] | |
| atoms0 = set() | |
| for p in learned0: | |
| atoms0 |= {i for i in range(6) if s.sig_bits(p)[i]} | |
| print(" 已学 perm 覆盖生成元原子: %s" % sorted(atoms0)) | |
| eff = {} | |
| for p in s.factor: | |
| y_no = s.acc(p, set(), noise=0.0) | |
| y_do = s.do_intervene(p, atoms0) | |
| eff[p] = y_do - y_no | |
| cand = sorted(eff, key=eff.get, reverse=True)[:3] | |
| print(" 干预(do)效应最大的 3 个候选 perm:") | |
| for p in cand: | |
| print(" %s +%.3f" % (p, round(eff[p], 3))) | |
| # ---- STEP 4: 因果发现 ---- | |
| show("STEP 4 因果发现 —— 从观测因子反估计 (生成元->泛化)") | |
| learned = [(0, 1, 2, 3), (1, 0, 2, 3), (0, 1, 3, 2), | |
| (0, 2, 1, 3), (0, 3, 2, 1)] | |
| disc = CausalDiscovery(s, alpha=0.5) | |
| # 因果发现:在"历史观测"上反推哪些因子决定泛化 | |
| # 为让离散统计稳定,对每个 perm 累积 8 次带噪观测 | |
| import numpy as np | |
| all_perms = list(s.factor.keys()) | |
| rows = [] | |
| k = 0 | |
| for p in all_perms: | |
| for _ in range(8): | |
| f = s.observe(p, rng=np.random.default_rng(1000 + k)) | |
| rows.append([f[0], f[1], f[2], s.acc(p, set(), seed=2000 + k)]) | |
| k += 1 | |
| Xs = np.array(rows) | |
| from hibs_lnn.causal.discovery import ci_test | |
| p_cyc, mi_cyc = ci_test(Xs, 2, 3, []) # cyc 与 acc | |
| p_gen, mi_gen = ci_test(Xs, 0, 3, []) # gen_count 与 acc | |
| print(" 历史观测样本数:", len(Xs)) | |
| print(" 条件独立检验 (p 越小越依赖):") | |
| print(" cyc(结构) -> acc : p=%.3f mi=%.3f -> %s" | |
| % (p_cyc, mi_cyc, "相关(depend)" if p_cyc < 0.3 else "独立")) | |
| print(" gen_count -> acc : p=%.3f mi=%.3f -> %s" | |
| % (p_gen, mi_gen, "相关(depend)" if p_gen < 0.3 else "独立")) | |
| print(' 解读:未覆盖任何生成元时,泛化仅由循环结构(cyc)决定;') | |
| print(' 这就是数据对【先学结构、再补生成元】的因果提示。') | |
| # ---- STEP 5: 识别 ---- | |
| show("STEP 5 识别 —— 后门调整") | |
| Gg = Graph({'cyc': {'G'}, 'obs': {'G', 'cyc'}, 'acc': {'obs', 'cyc'}}) | |
| Z = backdoor_adjustment_set(Gg, 'G', 'acc', {'G', 'cyc', 'obs'}) | |
| fident, msg = g_formula(Gg, 'G', 'acc', Z) | |
| print(" 后门调整集 Z = %r" % (Z,)) | |
| print(" 可识别性: %s" % msg) | |
| # ---- STEP 6: 反事实 ---- | |
| show("STEP 6 反事实 —— Pearl 三步 (Abduction/Action/Prediction)") | |
| obs_p = (1, 2, 3, 0) | |
| y_obs = s.acc(obs_p, atoms0, noise=0.05) | |
| y_cf, meta = s.counterfactual(obs_p, y_obs, atoms0, do_perm=(2, 0, 3, 1)) | |
| print(" 观测: perm=%s acc=%.3f" % (obs_p, y_obs)) | |
| print(" 反事实(若早期探索另一 perm、覆盖其生成元): acc=%.3f" % y_cf) | |
| print(" 归因: 推断外生扰动 u=%s, 生成元覆盖 %d -> %d" | |
| % (meta['u_acc'], meta['n_known_before'], meta['n_known_after'])) | |
| # ---- STEP 7: ★ 因果提议器 vs 启发式提议器 ---- | |
| show("STEP 7 ★ 因果提议器 vs 启发式提议器(自主探索效率对比)") | |
| scores = run_proposer_compare(s) | |
| print(" 在同样的已学集合上各提议 6 个 unseen 排列:") | |
| print(" %-22s %-16s %-14s" | |
| % ("提议器", "新生成元原子/步", "覆盖未见结构占比")) | |
| for name, new_atoms, frac in scores: | |
| print(" %-22s %-16s %-14s" | |
| % (name, "%.2f" % new_atoms, "%.2f" % frac)) | |
| print("\n 因果提议器优先选择【被生成元瓶颈卡住】的任务(do-效应/反事实增益高),") | |
| print(" 用更少探索步数覆盖更多【原因原子】——自主数据生成器的因果改进。") | |
| # ---- STEP 8: Causal RL ---- | |
| show("STEP 8 Causal RL —— 状态/动作/奖励 + 因果 credit assignment") | |
| agent = CausalRLAgent(learned[:3], k=1, seed=3) | |
| res = agent.run(4, verbose=True) | |
| print(" 归因到的原子级因果价值 Q:", res['q_atom']) | |
| print(" 已覆盖生成元原子数: %d / 6" % res['covered_atoms']) | |
| print("\n 完成:因果技术栈已在 headless 的 S4 世界完整落地。") | |
| print(" 详见 docs/wiki/causal_stack.md") | |
| def baseline_value(world, learned, p, freq): | |
| """启发式提议器价值(简约/自洽/覆盖的轻量复刻)。""" | |
| sim = 1.0 | |
| for q in learned: | |
| if world.cyc(p) == world.cyc(q): | |
| sim -= 0.3 | |
| learned_cyc = {world.cyc(q) for q in learned} | |
| con = 0.5 if world.cyc(p) in learned_cyc else 1.0 | |
| cov = 1.0 / (1 + freq.get(p, 0)) | |
| return sim + con + cov | |
| def run_proposer_compare(world, n_steps=6, n_learned=6): | |
| import numpy as np | |
| perms = list(world.factor.keys()) | |
| learned = perms[:n_learned] # 同一初始已学集合 | |
| cands = [p for p in perms if p not in learned] | |
| # --- 启发式 --- | |
| freq_b = {} | |
| picked_b = [] | |
| for _ in range(n_steps): | |
| sc = [baseline_value(world, learned, p, freq_b) for p in cands] | |
| for _ in range(4): | |
| bi = int(np.argmax(sc)) | |
| p = cands[bi] | |
| if p in picked_b: | |
| sc[bi] = -1e9 | |
| continue | |
| break | |
| picked_b.append(p) | |
| freq_b[p] = freq_b.get(p, 0) + 1 | |
| # --- 因果 --- | |
| cp = CausalProposer(learned, k=n_steps, seed=7) | |
| picked_c = cp.propose(n=n_steps) | |
| def _new_atoms(picked): | |
| at = set() | |
| for p in learned: | |
| at |= {i for i in range(6) if world.sig_bits(p)[i]} | |
| tot = 0 | |
| for p in picked: | |
| n0 = len(at) | |
| at |= {i for i in range(6) if world.sig_bits(p)[i]} | |
| tot += len(at) - n0 | |
| return tot / max(1, len(picked)) | |
| seen_cyc = {world.cyc(p) for p in learned} | |
| frac_b = sum(1 for p in picked_b if world.cyc(p) not in seen_cyc) / n_steps | |
| frac_c = sum(1 for p in picked_c if world.cyc(p) not in seen_cyc) / n_steps | |
| return [ | |
| ("启发式 (sim/con/cov)", _new_atoms(picked_b), frac_b), | |
| ("因果 (do/反事实/发现)", _new_atoms(picked_c), frac_c), | |
| ] | |
| if __name__ == "__main__": | |
| main() |