headless / causal_tech_stack_demo.py
logos42's picture
Upload folder using huggingface_hub
216004d verified
Raw History Blame Contribute Delete
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()