第57天:因果推理模型(二)- 高级算法与深度学习融合
🎯 第二部分:高级因果推理算法与深度学习应用
欢迎回到我们的因果推理之旅!在第一部分中,我们已经打下了坚实的理论基础,现在是时候进入更加exciting的高级领域了!如果说第一部分是"学会走路",那么第二部分就是"学会飞翔"------我们将探索如何将深度学习的强大能力与因果推理的严谨逻辑完美结合!
🚀 7. 双重机器学习(Double Machine Learning):现代因果推理的利器
双重机器学习是近年来因果推理领域最重要的突破之一!它巧妙地解决了一个古老的问题:如何在高维复杂数据中准确估计因果效应?
7.1 双重机器学习的核心思想
想象你是一个电商平台的分析师,想要估计"个性化推荐算法"对"用户购买金额"的因果效应。但是,用户有成千上万个特征(年龄、性别、历史购买记录、浏览行为等),传统方法很难处理这么高维的混淆变量。
双重机器学习的天才之处在于:它用机器学习来"去噪",用因果推理来"寻因"!
双重机器学习 vs 传统因果推理方法
核心差异对比表
维度 |
传统线性回归 |
传统因果推理 |
双重机器学习 (DML) |
处理维度 |
低维 (< 100) |
低-中维 (< 1000) |
高维 (> 10000) |
模型假设 |
线性关系 |
参数化模型 |
非参数/半参数 |
混淆处理 |
线性控制 |
因果图调整 |
ML自动去噪 |
预测精度 |
中等 |
中等 |
高 |
因果推断 |
有偏 |
无偏但有限制 |
渐近无偏 |
计算复杂度 |
O(n) |
O(n²) |
O(n log n) |
适用场景 |
简单业务 |
实验设计 |
复杂现实场景 |
DML的技术优势
技术特点 |
传统方法的问题 |
DML的解决方案 |
实际效果 |
双重去噪 |
单一模型偏差累积 |
分别估计m(x)和e(x) |
偏差相互抵消 |
交叉拟合 |
过拟合导致偏差 |
样本分割训练/预测 |
避免过拟合偏差 |
Neyman正交 |
扰动函数影响估计 |
正交化分数函数 |
对模型误设定稳健 |
机器学习集成 |
模型表达能力有限 |
任意ML算法 |
捕捉复杂非线性关系 |
数学框架对比
传统OLS回归:
复制代码
Y = αD + βX + ε
问题:高维X导致curse of dimensionality
双重机器学习:
复制代码
阶段1:估计 m(x) = E[Y|X=x], e(x) = E[D|X=x]
阶段2:求解 θ = argmin E[(Y - m(X) - θ(D - e(X)))²]
优势:θ̂对m(x)和e(x)的估计误差具有二阶robustness
适用场景分析
业务场景 |
传统方法适用性 |
DML适用性 |
推荐指数 |
A/B测试分析 |
⭐⭐⭐⭐ |
⭐⭐⭐ |
传统方法 |
个性化推荐效果 |
⭐⭐ |
⭐⭐⭐⭐⭐ |
DML |
广告投放ROI |
⭐⭐ |
⭐⭐⭐⭐⭐ |
DML |
价格策略优化 |
⭐⭐⭐ |
⭐⭐⭐⭐ |
DML |
用户流失预防 |
⭐ |
⭐⭐⭐⭐⭐ |
DML |
7.2 PyTorch实现双重机器学习
让我们用PyTorch构建一个完整的双重机器学习框架!这个实现将展示如何处理高维混淆变量,同时获得准确的因果效应估计。
python
复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import pandas as pd
from sklearn.model_selection import KFold
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from sklearn.linear_model import LogisticRegression, LinearRegression
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
# 设置随机种子
torch.manual_seed(42)
np.random.seed(42)
class DeepPropensityModel(nn.Module):
"""
深度倾向性得分模型:估计 e(x) = P(D=1|X=x)
使用深度神经网络捕捉复杂的非线性关系
"""
def __init__(self, input_dim, hidden_dims=[64, 32, 16], dropout_rate=0.2):
super(DeepPropensityModel, self).__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.extend([
nn.Linear(prev_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(dropout_rate)
])
prev_dim = hidden_dim
# 输出层(二分类)
layers.append(nn.Linear(prev_dim, 1))
layers.append(nn.Sigmoid())
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
class DeepOutcomeModel(nn.Module):
"""
深度结果模型:估计 m(x) = E[Y|X=x]
分别估计处理组和对照组的期望结果
"""
def __init__(self, input_dim, hidden_dims=[64, 32, 16], dropout_rate=0.2):
super(DeepOutcomeModel, self).__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.extend([
nn.Linear(prev_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(dropout_rate)
])
prev_dim = hidden_dim
# 输出层(回归)
layers.append(nn.Linear(prev_dim, 1))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
class DoubleMachineLearning:
"""
双重机器学习框架
实现Chernozhukov等人(2018)提出的DML算法
"""
def __init__(self, n_folds=3, random_state=42):
self.n_folds = n_folds
self.random_state = random_state
self.kfold = KFold(n_splits=n_folds, shuffle=True, random_state=random_state)
# 存储结果
self.ate_estimates = []
self.propensity_models = []
self.outcome_models = []
self.residuals = {'Y': [], 'D': []}
def fit(self, X, D, Y, ml_method='deep', epochs=100, lr=0.001, verbose=True):
"""
拟合双重机器学习模型
Args:
X: 特征矩阵 (n_samples, n_features)
D: 处理变量 (n_samples,) - 二元变量
Y: 结果变量 (n_samples,)
ml_method: 机器学习方法 ('deep', 'rf', 'linear')
epochs: 深度学习训练轮数
lr: 学习率
"""
if verbose:
print("🚀 开始双重机器学习训练...")
print(f"📊 数据规模: {X.shape[0]} 样本, {X.shape[1]} 特征")
print(f"🔄 交叉验证折数: {self.n_folds}")
print(f"🧠 ML方法: {ml_method}")
X_tensor = torch.tensor(X, dtype=torch.float32)
D_tensor = torch.tensor(D, dtype=torch.float32)
Y_tensor = torch.tensor(Y, dtype=torch.float32)
# 存储所有fold的残差
all_Y_residuals = np.zeros_like(Y)
all_D_residuals = np.zeros_like(D)
fold_results = []
for fold_idx, (train_idx, test_idx) in enumerate(self.kfold.split(X)):
if verbose:
print(f"\n📁 处理第 {fold_idx + 1} 折...")
# 分割数据
X_train, X_test = X_tensor[train_idx], X_tensor[test_idx]
D_train, D_test = D_tensor[train_idx], D_tensor[test_idx]
Y_train, Y_test = Y_tensor[train_idx], Y_tensor[test_idx]
# 1. 训练倾向性得分模型 e(x) = E[D|X]
propensity_model = self._train_propensity_model(
X_train, D_train, ml_method, epochs, lr
)
# 2. 训练结果模型 m(x) = E[Y|X] (对于D=0和D=1分别训练)
outcome_model_0 = self._train_outcome_model(
X_train[D_train == 0], Y_train[D_train == 0], ml_method, epochs, lr
)
outcome_model_1 = self._train_outcome_model(
X_train[D_train == 1], Y_train[D_train == 1], ml_method, epochs, lr
)
# 3. 在测试集上预测并计算残差
with torch.no_grad():
# 倾向性得分预测
e_pred = propensity_model(X_test).squeeze()
# 结果预测
m0_pred = outcome_model_0(X_test).squeeze()
m1_pred = outcome_model_1(X_test).squeeze()
# 计算残差(Neyman正交条件的核心)
# Y残差: Y - m(X)
Y_residual = Y_test - (D_test * m1_pred + (1 - D_test) * m0_pred)
# D残差: D - e(X)
D_residual = D_test - e_pred
# 存储残差
all_Y_residuals[test_idx] = Y_residual.numpy()
all_D_residuals[test_idx] = D_residual.numpy()
# 存储模型
self.propensity_models.append(propensity_model)
self.outcome_models.append((outcome_model_0, outcome_model_1))
fold_results.append({
'propensity_score': e_pred.numpy(),
'outcome_0': m0_pred.numpy(),
'outcome_1': m1_pred.numpy(),
'Y_residual': Y_residual.numpy(),
'D_residual': D_residual.numpy()
})
# 4. 最终的ATE估计(基于Neyman正交条件)
# θ = E[ψ(W,θ)] = 0 的解,其中ψ是正交分数函数
# 这里使用简单的矩估计:E[Y_residual * D_residual] / E[D_residual²]
numerator = np.mean(all_Y_residuals * all_D_residuals)
denominator = np.mean(all_D_residuals ** 2)
if abs(denominator) < 1e-8:
self.ate_estimate = 0.0
if verbose:
print("⚠️ 警告:分母接近零,ATE估计可能不稳定")
else:
self.ate_estimate = numerator / denominator
# 计算标准误
self.ate_se = self._calculate_standard_error(all_Y_residuals, all_D_residuals)
# 存储残差用于后续分析
self.residuals['Y'] = all_Y_residuals
self.residuals['D'] = all_D_residuals
if verbose:
print(f"\n✅ 训练完成!")
print(f"📊 ATE估计: {self.ate_estimate:.4f} ± {self.ate_se:.4f}")
return self
def _train_propensity_model(self, X_train, D_train, ml_method, epochs, lr):
"""训练倾向性得分模型"""
if ml_method == 'deep':
model = DeepPropensityModel(X_train.shape[1])
optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=1e-5)
criterion = nn.BCELoss()
for epoch in range(epochs):
optimizer.zero_grad()
pred = model(X_train).squeeze()
loss = criterion(pred, D_train)
loss.backward()
optimizer.step()
return model
elif ml_method == 'rf':
# 使用sklearn的随机森林(需要转换为numpy)
model = RandomForestClassifier(n_estimators=100, random_state=self.random_state)
model.fit(X_train.numpy(), D_train.numpy())
return model
else: # linear
model = LogisticRegression(random_state=self.random_state)
model.fit(X_train.numpy(), D_train.numpy())
return model
def _train_outcome_model(self, X_train, Y_train, ml_method, epochs, lr):
"""训练结果模型"""
if len(X_train) == 0: # 处理空数据的情况
return self._create_dummy_model(X_train.shape[1] if len(X_train.shape) > 1 else 1)
if ml_method == 'deep':
model = DeepOutcomeModel(X_train.shape[1])
optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=1e-5)
criterion = nn.MSELoss()
for epoch in range(epochs):
optimizer.zero_grad()
pred = model(X_train).squeeze()
loss = criterion(pred, Y_train)
loss.backward()
optimizer.step()
return model
elif ml_method == 'rf':
model = RandomForestRegressor(n_estimators=100, random_state=self.random_state)
model.fit(X_train.numpy(), Y_train.numpy())
return model
else: # linear
model = LinearRegression()
model.fit(X_train.numpy(), Y_train.numpy())
return model
def _create_dummy_model(self, input_dim):
"""创建虚拟模型处理边界情况"""
class DummyModel(nn.Module):
def __init__(self):
super().__init__()
self.dummy = nn.Parameter(torch.zeros(1))
def forward(self, x):
return torch.zeros(x.shape[0])
return DummyModel()
def _calculate_standard_error(self, Y_residuals, D_residuals):
"""计算ATE的标准误"""
try:
n = len(Y_residuals)
# 使用影响函数计算标准误
psi = Y_residuals * D_residuals - self.ate_estimate * (D_residuals ** 2)
variance = np.var(psi)
standard_error = np.sqrt(variance / n)
return standard_error
except:
return 0.0
def predict_individual_effects(self, X_new):
"""预测个体处理效应 CATE(x) = E[Y(1) - Y(0)|X=x]"""
X_tensor = torch.tensor(X_new, dtype=torch.float32)
individual_effects = []
for fold_idx in range(len(self.outcome_models)):
outcome_model_0, outcome_model_1 = self.outcome_models[fold_idx]
with torch.no_grad():
if hasattr(outcome_model_0, 'forward'): # PyTorch模型
y0_pred = outcome_model_0(X_tensor).squeeze()
y1_pred = outcome_model_1(X_tensor).squeeze()
else: # sklearn模型
y0_pred = torch.tensor(outcome_model_0.predict(X_new))
y1_pred = torch.tensor(outcome_model_1.predict(X_new))
effect = y1_pred - y0_pred
individual_effects.append(effect.numpy())
# 平均所有fold的预测
return np.mean(individual_effects, axis=0)
def get_confidence_interval(self, alpha=0.05):
"""计算置信区间"""
z_score = stats.norm.ppf(1 - alpha/2)
lower = self.ate_estimate - z_score * self.ate_se
upper = self.ate_estimate + z_score * self.ate_se
return lower, upper
def visualize_results(self):
"""可视化DML结果"""
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# 1. 残差分布
axes[0, 0].hist(self.residuals['Y'], bins=30, alpha=0.7, color='skyblue',
label='Y残差', density=True)
axes[0, 0].set_title('结果变量残差分布')
axes[0, 0].set_xlabel('Y残差')
axes[0, 0].set_ylabel('密度')
axes[0, 0].grid(True, alpha=0.3)
# 2. 处理变量残差分布
axes[0, 1].hist(self.residuals['D'], bins=30, alpha=0.7, color='lightcoral',
label='D残差', density=True)
axes[0, 1].set_title('处理变量残差分布')
axes[0, 1].set_xlabel('D残差')
axes[0, 1].set_ylabel('密度')
axes[0, 1].grid(True, alpha=0.3)
# 3. 残差散点图(检查正交性)
axes[1, 0].scatter(self.residuals['D'], self.residuals['Y'], alpha=0.6, s=10)
axes[1, 0].set_title('残差散点图(检查正交性)')
axes[1, 0].set_xlabel('D残差')
axes[1, 0].set_ylabel('Y残差')
axes[1, 0].grid(True, alpha=0.3)
# 添加回归线
z = np.polyfit(self.residuals['D'], self.residuals['Y'], 1)
p = np.poly1d(z)
axes[1, 0].plot(self.residuals['D'], p(self.residuals['D']), "r--", alpha=0.8)
# 4. ATE估计结果
lower, upper = self.get_confidence_interval()
axes[1, 1].bar(['ATE估计'], [self.ate_estimate], yerr=[self.ate_se],
capsize=10, color='gold', alpha=0.8)
axes[1, 1].set_title(f'ATE估计: {self.ate_estimate:.4f} ± {self.ate_se:.4f}')
axes[1, 1].set_ylabel('处理效应')
axes[1, 1].grid(True, alpha=0.3)
# 添加置信区间文本
axes[1, 1].text(0, self.ate_estimate + self.ate_se + 0.1,
f'95% CI: [{lower:.3f}, {upper:.3f}]',
ha='center', fontsize=10)
plt.tight_layout()
plt.show()
def generate_complex_causal_data(n_samples=2000, n_features=20, true_ate=1.5):
"""
生成复杂的高维因果数据
模拟现实世界中的复杂因果关系
"""
np.random.seed(42)
# 1. 生成基础特征(混淆变量)
X = np.random.randn(n_samples, n_features)
# 2. 添加一些非线性特征交互
X[:, 0] = X[:, 0] ** 2 # 二次项
X[:, 1] = np.sin(X[:, 1]) # 三角函数
X[:, 2] = np.exp(X[:, 2] / 3) # 指数项
# 3. 生成复杂的倾向性得分(非线性)
linear_score = (X[:, :5] @ np.array([0.5, -0.3, 0.2, 0.4, -0.6]) +
0.2 * X[:, 0] * X[:, 1] + # 交互项
0.1 * np.sum(X[:, :3] ** 2, axis=1)) # 非线性项
propensity_score = 1 / (1 + np.exp(-linear_score))
D = np.random.binomial(1, propensity_score)
# 4. 生成复杂的结果变量
# 基础效应
base_effect = (X[:, :8] @ np.array([1.0, -0.5, 0.8, 0.3, -0.7, 0.4, 0.6, -0.2]) +
0.3 * X[:, 0] * X[:, 2] + # 交互项
0.15 * np.sum(X[:, 5:8] ** 2, axis=1)) # 非线性项
# 处理效应(异质性)
heterogeneous_effect = true_ate + 0.5 * X[:, 0] - 0.3 * X[:, 1]
# 最终结果
Y = base_effect + D * heterogeneous_effect + np.random.normal(0, 0.5, n_samples)
return X, D, Y, true_ate
def run_dml_experiment():
"""运行完整的DML实验"""
print("🎯 双重机器学习实验开始!")
print("=" * 60)
# 1. 生成复杂数据
print("\n📊 生成高维复杂因果数据...")
X, D, Y, true_ate = generate_complex_causal_data(n_samples=2000, n_features=20, true_ate=1.5)
print(f"✅ 数据生成完成:")
print(f" - 样本数: {len(X)}")
print(f" - 特征维度: {X.shape[1]}")
print(f" - 处理率: {D.mean():.3f}")
print(f" - 真实ATE: {true_ate}")
# 2. 对比不同方法
methods = {
'DML-Deep': {'ml_method': 'deep', 'epochs': 150},
'DML-RF': {'ml_method': 'rf', 'epochs': 0},
'DML-Linear': {'ml_method': 'linear', 'epochs': 0}
}
results = {}
for method_name, params in methods.items():
print(f"\n🧠 训练 {method_name}...")
dml = DoubleMachineLearning(n_folds=3, random_state=42)
dml.fit(X, D, Y, verbose=False, **params)
lower, upper = dml.get_confidence_interval()
bias = abs(dml.ate_estimate - true_ate)
results[method_name] = {
'ate_estimate': dml.ate_estimate,
'standard_error': dml.ate_se,
'confidence_interval': (lower, upper),
'bias': bias,
'model': dml
}
print(f" ATE估计: {dml.ate_estimate:.4f} ± {dml.ate_se:.4f}")
print(f" 95% CI: [{lower:.3f}, {upper:.3f}]")
print(f" 偏差: {bias:.4f}")
# 3. 朴素估计作为基准
naive_ate = Y[D==1].mean() - Y[D==0].mean()
naive_bias = abs(naive_ate - true_ate)
print(f"\n📊 基准比较:")
print(f" 朴素估计: {naive_ate:.4f} (偏差: {naive_bias:.4f})")
print(f" 真实ATE: {true_ate:.4f}")
# 4. 可视化最佳方法的结果
best_method = min(results.items(), key=lambda x: x[1]['bias'])
print(f"\n🏆 最佳方法: {best_method[0]} (偏差: {best_method[1]['bias']:.4f})")
best_method[1]['model'].visualize_results()
return results, X, D, Y, true_ate
# 运行实验
if __name__ == "__main__":
results, X, D, Y, true_ate = run_dml_experiment()
🌲 8. 因果森林(Causal Forest):发现异质性处理效应
如果说双重机器学习是用来估计"平均处理效应"的利器,那么因果森林就是发现"个性化处理效应"的神器!想象一下,同样是推荐算法,对不同用户的效果可能完全不同------年轻用户可能更容易被新奇的内容吸引,而年长用户可能更偏好经典内容。
8.1 异质性处理效应的重要性
异质性处理效应:从平均到个体
平均效应 vs 个体效应对比
维度 |
平均处理效应 (ATE) |
条件平均处理效应 (CATE) |
定义 |
E[Y(1) - Y(0)] |
E[Y(1) - Y(0)|X=x] |
解释 |
所有个体的平均效应 |
特定特征下的平均效应 |
信息量 |
低(一个数字) |
高(函数形式) |
决策价值 |
群体层面决策 |
个体层面决策 |
估计难度 |
相对简单 |
高度复杂 |
业务场景中的异质性效应
业务场景 |
平均效应结论 |
异质性效应发现 |
商业价值 |
个性化推荐 |
推荐算法平均提升15%点击率 |
年轻用户+25%,年长用户+5% |
分群策略优化 |
广告投放 |
广告平均ROI为1.2 |
高收入群体ROI=2.1,低收入群体ROI=0.8 |
精准投放预算分配 |
药物治疗 |
新药平均疗效60% |
基因型A疗效85%,基因型B疗效35% |
精准医疗 |
教育干预 |
在线课程平均提升12分 |
基础好的学生+20分,基础差的学生+4分 |
分层教学设计 |
价格策略 |
降价平均增加20%销量 |
价格敏感用户+35%,品牌忠诚用户+5% |
动态定价策略 |
异质性的数学表达
简单异质性(线性)
复制代码
τ(x) = α + βx
其中 α 是基础效应,β 是异质性系数
复杂异质性(非线性)
复制代码
τ(x) = f(x)
其中 f(·) 是任意非线性函数
因果森林的优势
复制代码
τ̂(x) = 非参数估计,能够捕捉任意复杂的异质性模式
检测异质性的方法
方法 |
原理 |
优势 |
劣势 |
适用场景 |
分层分析 |
按特征分组比较 |
简单直观 |
维度诅咒 |
低维特征 |
交互项回归 |
添加处理×特征交互项 |
参数化解释 |
需要预先指定 |
已知异质性方向 |
因果树 |
递归分割寻找异质性 |
非参数 |
过拟合风险 |
探索性分析 |
因果森林 |
集成多个因果树 |
高精度+稳定性 |
计算复杂 |
高维复杂场景 |
Meta-Learner |
两阶段学习策略 |
灵活性高 |
理论保证弱 |
ML方法集成 |
8.2 PyTorch实现因果森林
python
复制代码
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
class CausalTree:
"""
因果树:专门用于估计异质性处理效应的决策树
实现 Athey & Imbens (2016) 的 Honest Causal Tree
"""
def __init__(self, min_samples_split=20, min_samples_leaf=10, max_depth=5,
honest=True, alpha=0.05):
self.min_samples_split = min_samples_split
self.min_samples_leaf = min_samples_leaf
self.max_depth = max_depth
self.honest = honest # 是否使用honest splitting
self.alpha = alpha # 统计显著性水平
self.tree_structure = None
self.feature_importances_ = None
class Node:
def __init__(self):
self.feature_idx = None
self.threshold = None
self.left = None
self.right = None
self.is_leaf = False
self.treatment_effect = None
self.n_samples = 0
self.n_treated = 0
self.n_control = 0
self.prediction_data = None # 用于honest splitting的预测数据
def fit(self, X, D, Y):
"""
训练因果树
Args:
X: 特征矩阵 (n_samples, n_features)
D: 处理变量 (n_samples,)
Y: 结果变量 (n_samples,)
"""
self.n_features = X.shape[1]
self.feature_importances_ = np.zeros(self.n_features)
if self.honest:
# Honest splitting: 将数据分为结构构建和效应估计两部分
X_struct, X_pred, D_struct, D_pred, Y_struct, Y_pred = train_test_split(
X, D, Y, test_size=0.5, random_state=42, stratify=D
)
# 使用结构数据构建树结构
self.tree_structure = self._build_tree(X_struct, D_struct, Y_struct,
X_pred, D_pred, Y_pred, depth=0)
else:
# 传统方法:使用全部数据
self.tree_structure = self._build_tree(X, D, Y, X, D, Y, depth=0)
return self
def _build_tree(self, X_struct, D_struct, Y_struct, X_pred, D_pred, Y_pred, depth):
"""递归构建因果树"""
node = self.Node()
node.n_samples = len(X_struct)
node.n_treated = int(D_struct.sum())
node.n_control = node.n_samples - node.n_treated
# 存储预测数据用于效应估计
node.prediction_data = (X_pred, D_pred, Y_pred)
# 停止条件检查
if (depth >= self.max_depth or
node.n_samples < self.min_samples_split or
node.n_treated < self.min_samples_leaf or
node.n_control < self.min_samples_leaf):
node.is_leaf = True
node.treatment_effect = self._estimate_leaf_effect(D_pred, Y_pred)
return node
# 寻找最佳分割
best_split = self._find_best_split(X_struct, D_struct, Y_struct)
if best_split is None:
node.is_leaf = True
node.treatment_effect = self._estimate_leaf_effect(D_pred, Y_pred)
return node
# 应用分割
node.feature_idx = best_split['feature_idx']
node.threshold = best_split['threshold']
# 更新特征重要性
self.feature_importances_[node.feature_idx] += best_split['importance']
# 分割结构数据
left_mask_struct = X_struct[:, node.feature_idx] <= node.threshold
right_mask_struct = ~left_mask_struct
# 分割预测数据
left_mask_pred = X_pred[:, node.feature_idx] <= node.threshold
right_mask_pred = ~left_mask_pred
# 递归构建子树
node.left = self._build_tree(
X_struct[left_mask_struct], D_struct[left_mask_struct], Y_struct[left_mask_struct],
X_pred[left_mask_pred], D_pred[left_mask_pred], Y_pred[left_mask_pred],
depth + 1
)
node.right = self._build_tree(
X_struct[right_mask_struct], D_struct[right_mask_struct], Y_struct[right_mask_struct],
X_pred[right_mask_pred], D_pred[right_mask_pred], Y_pred[right_mask_pred],
depth + 1
)
return node
def _find_best_split(self, X, D, Y):
"""寻找最佳分割点,最大化处理效应的异质性"""
best_split = None
best_heterogeneity = -np.inf
# 随机选择特征子集(类似随机森林)
n_features_to_try = max(1, int(np.sqrt(self.n_features)))
features_to_try = np.random.choice(self.n_features, n_features_to_try, replace=False)
for feature_idx in features_to_try:
# 获取该特征的唯一值作为候选分割点
unique_values = np.unique(X[:, feature_idx])
if len(unique_values) <= 1:
continue
# 尝试所有可能的分割点
for i in range(len(unique_values) - 1):
threshold = (unique_values[i] + unique_values[i + 1]) / 2
# 分割数据
left_mask = X[:, feature_idx] <= threshold
right_mask = ~left_mask
# 检查分割后的样本量
if (left_mask.sum() < self.min_samples_leaf or
right_mask.sum() < self.min_samples_leaf):
continue
# 检查处理组和对照组的分布
left_treated = D[left_mask].sum()
left_control = left_mask.sum() - left_treated
right_treated = D[right_mask].sum()
right_control = right_mask.sum() - right_treated
if (left_treated < 2 or left_control < 2 or
right_treated < 2 or right_control < 2):
continue
# 计算异质性增益
heterogeneity = self._calculate_heterogeneity_gain(
D, Y, left_mask, right_mask
)
if heterogeneity > best_heterogeneity:
best_heterogeneity = heterogeneity
best_split = {
'feature_idx': feature_idx,
'threshold': threshold,
'heterogeneity': heterogeneity,
'importance': heterogeneity
}
return best_split
def _calculate_heterogeneity_gain(self, D, Y, left_mask, right_mask):
"""计算异质性增益:分割后处理效应方差的减少"""
try:
# 父节点的处理效应
parent_effect = self._estimate_treatment_effect(D, Y)
# 左子节点的处理效应
left_effect = self._estimate_treatment_effect(D[left_mask], Y[left_mask])
# 右子节点的处理效应
right_effect = self._estimate_treatment_effect(D[right_mask], Y[right_mask])
# 计算加权方差减少
n_total = len(D)
n_left = left_mask.sum()
n_right = right_mask.sum()
# 异质性增益 = 分割前的方差 - 分割后的加权方差
heterogeneity_gain = abs(left_effect - right_effect) * min(n_left, n_right) / n_total
return heterogeneity_gain
except:
return 0.0
def _estimate_treatment_effect(self, D, Y):
"""估计处理效应"""
if len(D) == 0:
return 0.0
treated_mask = D == 1
control_mask = D == 0
if treated_mask.sum() == 0 or control_mask.sum() == 0:
return 0.0
treated_outcome = Y[treated_mask].mean()
control_outcome = Y[control_mask].mean()
return treated_outcome - control_outcome
def _estimate_leaf_effect(self, D, Y):
"""估计叶子节点的处理效应"""
return self._estimate_treatment_effect(D, Y)
def predict(self, X):
"""预测处理效应"""
if self.tree_structure is None:
raise ValueError("模型未训练,请先调用fit方法")
predictions = np.zeros(len(X))
for i, x in enumerate(X):
predictions[i] = self._predict_single(x, self.tree_structure)
return predictions
def _predict_single(self, x, node):
"""预测单个样本的处理效应"""
if node.is_leaf:
return node.treatment_effect
if x[node.feature_idx] <= node.threshold:
return self._predict_single(x, node.left)
else:
return self._predict_single(x, node.right)
class CausalForest:
"""
因果森林:集成多个因果树来估计异质性处理效应
实现 Wager & Athey (2018) 的 Generalized Random Forest
"""
def __init__(self, n_estimators=100, max_depth=5, min_samples_split=20,
min_samples_leaf=10, max_features='sqrt', bootstrap=True,
honest=True, random_state=None):
self.n_estimators = n_estimators
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.min_samples_leaf = min_samples_leaf
self.max_features = max_features
self.bootstrap = bootstrap
self.honest = honest
self.random_state = random_state
self.trees = []
self.feature_importances_ = None
if random_state is not None:
np.random.seed(random_state)
def fit(self, X, D, Y, verbose=True):
"""训练因果森林"""
if verbose:
print(f"🌲 开始训练因果森林...")
print(f" - 树的数量: {self.n_estimators}")
print(f" - 最大深度: {self.max_depth}")
print(f" - 是否Honest: {self.honest}")
self.n_features = X.shape[1]
self.feature_importances_ = np.zeros(self.n_features)
self.trees = []
for i in range(self.n_estimators):
if verbose and (i + 1) % 20 == 0:
print(f" 训练进度: {i+1}/{self.n_estimators}")
# Bootstrap采样
if self.bootstrap:
indices = np.random.choice(len(X), len(X), replace=True)
X_bootstrap = X[indices]
D_bootstrap = D[indices]
Y_bootstrap = Y[indices]
else:
X_bootstrap, D_bootstrap, Y_bootstrap = X, D, Y
# 训练单个因果树
tree = CausalTree(
min_samples_split=self.min_samples_split,
min_samples_leaf=self.min_samples_leaf,
max_depth=self.max_depth,
honest=self.honest
)
tree.fit(X_bootstrap, D_bootstrap, Y_bootstrap)
self.trees.append(tree)
# 累积特征重要性
if tree.feature_importances_ is not None:
self.feature_importances_ += tree.feature_importances_
# 标准化特征重要性
if self.feature_importances_.sum() > 0:
self.feature_importances_ /= self.feature_importances_.sum()
if verbose:
print("✅ 因果森林训练完成!")
return self
def predict(self, X):
"""预测异质性处理效应"""
if not self.trees:
raise ValueError("模型未训练,请先调用fit方法")
# 收集所有树的预测
predictions = np.zeros((len(X), len(self.trees)))
for i, tree in enumerate(self.trees):
try:
predictions[:, i] = tree.predict(X)
except:
predictions[:, i] = 0 # 处理预测失败的情况
# 返回平均预测
return predictions.mean(axis=1)
def predict_with_uncertainty(self, X):
"""预测处理效应并给出不确定性估计"""
predictions = np.zeros((len(X), len(self.trees)))
for i, tree in enumerate(self.trees):
try:
predictions[:, i] = tree.predict(X)
except:
predictions[:, i] = 0
mean_prediction = predictions.mean(axis=1)
std_prediction = predictions.std(axis=1)
return mean_prediction, std_prediction
def get_feature_importance(self):
"""获取特征重要性"""
return self.feature_importances_
def generate_heterogeneous_data(n_samples=2000, n_features=10):
"""生成具有异质性处理效应的数据"""
np.random.seed(42)
# 生成特征
X = np.random.randn(n_samples, n_features)
# 生成倾向性得分(简单线性)
propensity_score = 1 / (1 + np.exp(-(X[:, 0] + 0.5 * X[:, 1])))
D = np.random.binomial(1, propensity_score)
# 生成异质性处理效应
# 真实的CATE函数:τ(x) = 1 + 2*x₀ - x₁ + 0.5*x₀*x₁
true_cate = 1 + 2 * X[:, 0] - X[:, 1] + 0.5 * X[:, 0] * X[:, 1]
# 生成结果变量
# Y = μ(X) + τ(X) * D + ε
base_outcome = X[:, :3].sum(axis=1) # 基础结果函数
Y = base_outcome + D * true_cate + np.random.normal(0, 0.5, n_samples)
return X, D, Y, true_cate
def compare_causal_methods():
"""对比不同因果推理方法在异质性效应估计上的表现"""
print("🎯 异质性处理效应估计方法对比实验")
print("=" * 60)
# 1. 生成数据
print("\n📊 生成异质性数据...")
X, D, Y, true_cate = generate_heterogeneous_data(n_samples=1500, n_features=10)
# 分割训练集和测试集
X_train, X_test, D_train, D_test, Y_train, Y_test, cate_train, cate_test = train_test_split(
X, D, Y, true_cate, test_size=0.3, random_state=42
)
print(f"✅ 数据生成完成:")
print(f" - 训练集: {len(X_train)} 样本")
print(f" - 测试集: {len(X_test)} 样本")
print(f" - 特征维度: {X.shape[1]}")
print(f" - 真实CATE范围: [{true_cate.min():.2f}, {true_cate.max():.2f}]")
# 2. 训练不同模型
methods = {}
# 因果森林
print("\n🌲 训练因果森林...")
cf = CausalForest(n_estimators=50, max_depth=4, honest=True, random_state=42)
cf.fit(X_train, D_train, Y_train, verbose=False)
cf_pred = cf.predict(X_test)
methods['Causal Forest'] = cf_pred
# 朴素方法:分组计算差异
print("📊 计算朴素估计...")
naive_cate = np.full(len(X_test), Y_train[D_train==1].mean() - Y_train[D_train==0].mean())
methods['Naive (ATE)'] = naive_cate
# S-Learner:单一模型学习Y ~ X, D
print("🧠 训练S-Learner...")
from sklearn.ensemble import RandomForestRegressor
s_learner = RandomForestRegressor(n_estimators=50, random_state=42)
X_train_with_D = np.column_stack([X_train, D_train])
s_learner.fit(X_train_with_D, Y_train)
X_test_with_1 = np.column_stack([X_test, np.ones(len(X_test))])
X_test_with_0 = np.column_stack([X_test, np.zeros(len(X_test))])
s_pred_1 = s_learner.predict(X_test_with_1)
s_pred_0 = s_learner.predict(X_test_with_0)
methods['S-Learner'] = s_pred_1 - s_pred_0
# 3. 评估性能
print("\n📊 性能评估结果:")
print("-" * 50)
results = {}
for method_name, predictions in methods.items():
mse = np.mean((predictions - cate_test) ** 2)
mae = np.mean(np.abs(predictions - cate_test))
r2 = 1 - mse / np.var(cate_test)
results[method_name] = {'MSE': mse, 'MAE': mae, 'R²': r2}
print(f"{method_name:15s}: MSE={mse:.4f}, MAE={mae:.4f}, R²={r2:.4f}")
# 4. 可视化结果
print("\n📈 生成对比图表...")
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
# 真实值 vs 预测值散点图
for i, (method_name, predictions) in enumerate(methods.items()):
ax = axes[i//2, i%2]
ax.scatter(cate_test, predictions, alpha=0.6, s=20)
ax.plot([cate_test.min(), cate_test.max()], [cate_test.min(), cate_test.max()],
'r--', lw=2, label='完美预测')
ax.set_xlabel('真实CATE')
ax.set_ylabel('预测CATE')
ax.set_title(f'{method_name}\nR² = {results[method_name]["R²"]:.3f}')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 5. 特征重要性分析(仅针对因果森林)
if hasattr(cf, 'feature_importances_'):
plt.figure(figsize=(10, 6))
feature_names = [f'X{i}' for i in range(X.shape[1])]
plt.bar(feature_names, cf.feature_importances_)
plt.title('因果森林特征重要性')
plt.xlabel('特征')
plt.ylabel('重要性')
plt.xticks(rotation=45)
plt.grid(True, alpha=0.3)
plt.show()
return results, methods, cf
# 运行对比实验
if __name__ == "__main__":
results, methods, cf_model = compare_causal_methods()
🔮 9. 反事实推理的深度实现:构建时光机器
反事实推理是因果推理的最高境界!它不仅要求我们理解"如果做X会怎样"(干预推理),还要能回答"如果当时没做X会怎样"(反事实推理)。这就像是给我们的AI模型装上了"时光机器",能够回到过去重新做决定!
9.1 反事实推理的数学框架
反事实推理:从理论到实践
三层因果推理对比
层次 |
问题类型 |
数学表达 |
现实例子 |
技术挑战 |
观测层 |
看到了什么? |
P(Y|X) |
用户点击了推荐商品 |
相关性 ≠ 因果性 |
干预层 |
如果干预会怎样? |
P(Y|do(X)) |
如果推荐算法A替代B? |
需要随机实验 |
反事实层 |
如果当时不这样会怎样? |
P(Y_x|X',Y') |
如果用户没收到推荐会买吗? |
无法直接观测 |
反事实推理的核心要素
组件 |
符号表示 |
含义 |
实例 |
因果模型 |
M |
描述变量间因果关系的结构 |
SCM(结构因果模型) |
观测证据 |
e = (X=x, Y=y) |
实际观测到的事实 |
用户A购买了商品B |
反事实干预 |
do(X=x') |
假想的干预操作 |
假如推荐商品C而不是B |
反事实结果 |
Y_{x'}(u) |
在干预下的假想结果 |
用户A还会购买吗? |
反事实推理的三步法则
步骤 |
名称 |
操作 |
目标 |
第一步 |
溯因(Abduction) |
基于观测推断未观测变量 |
确定噪声项U的值 |
第二步 |
行动(Action) |
应用反事实干预 |
修改因果模型 |
第三步 |
预测(Prediction) |
在新模型下计算结果 |
得到反事实结果 |
技术实现方法对比
方法 |
优势 |
劣势 |
适用场景 |
结构因果模型 |
理论严谨、可解释性强 |
需要先验知识 |
已知因果结构 |
深度生成模型 |
表达能力强、端到端 |
黑盒、难以解释 |
高维复杂数据 |
因果GAN |
能生成反事实样本 |
训练不稳定 |
图像、文本数据 |
变分推断 |
处理不确定性 |
计算复杂 |
贝叶斯因果推理 |
反事实推理的关键挑战
挑战 |
描述 |
解决思路 |
根本因果问题 |
反事实不可直接观测 |
使用因果假设+模型 |
模型识别问题 |
多个模型可能拟合同一数据 |
增加先验约束 |
高维诅咒 |
特征维度过高难以建模 |
降维+表示学习 |
因果图未知 |
不知道真实的因果结构 |
因果发现算法 |
未观测混淆 |
存在隐藏的混淆变量 |
工具变量/自然实验 |
9.2 PyTorch实现深度反事实推理
python
复制代码
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.distributions import Normal, Bernoulli
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import warnings
warnings.filterwarnings('ignore')
# 设置随机种子
torch.manual_seed(42)
np.random.seed(42)
class CausalVAE(nn.Module):
"""
因果变分自编码器:学习因果表示并支持反事实推理
结合VAE的表示学习能力和结构因果模型的可解释性
"""
def __init__(self, input_dim, latent_dim=10, hidden_dim=64):
super(CausalVAE, self).__init__()
self.input_dim = input_dim
self.latent_dim = latent_dim
self.hidden_dim = hidden_dim
# 编码器:X -> Z (潜在混淆变量)
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim//2),
nn.ReLU()
)
# 潜在空间的均值和方差
self.mu_layer = nn.Linear(hidden_dim//2, latent_dim)
self.logvar_layer = nn.Linear(hidden_dim//2, latent_dim)
# 结构因果模型组件
# Z -> X (特征生成)
self.feature_decoder = nn.Sequential(
nn.Linear(latent_dim, hidden_dim//2),
nn.ReLU(),
nn.Linear(hidden_dim//2, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, input_dim)
)
# Z -> D (处理分配机制)
self.treatment_model = nn.Sequential(
nn.Linear(latent_dim, hidden_dim//4),
nn.ReLU(),
nn.Linear(hidden_dim//4, 1),
nn.Sigmoid()
)
# Z, D -> Y (结果生成机制)
self.outcome_model = nn.Sequential(
nn.Linear(latent_dim + 1, hidden_dim//2),
nn.ReLU(),
nn.Linear(hidden_dim//2, hidden_dim//4),
nn.ReLU(),
nn.Linear(hidden_dim//4, 1)
)
def encode(self, x):
"""编码:X -> Z的分布参数"""
h = self.encoder(x)
mu = self.mu_layer(h)
logvar = self.logvar_layer(h)
return mu, logvar
def reparameterize(self, mu, logvar):
"""重参数化技巧:从潜在分布中采样"""
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
def forward(self, x, d, y=None):
"""
前向传播:完整的因果生成过程
Args:
x: 特征 [batch_size, input_dim]
d: 处理变量 [batch_size, 1]
y: 结果变量 [batch_size, 1] (可选)
Returns:
重构结果和相关损失项
"""
# 1. 编码:推断潜在混淆变量
mu, logvar = self.encode(x)
z = self.reparameterize(mu, logvar)
# 2. 因果生成过程
# Z -> X (特征重构)
x_recon = self.feature_decoder(z)
# Z -> D (处理分配)
d_logits = self.treatment_model(z)
# Z, D -> Y (结果生成)
zd = torch.cat([z, d], dim=1)
y_pred = self.outcome_model(zd)
return {
'x_recon': x_recon,
'd_prob': d_logits,
'y_pred': y_pred,
'mu': mu,
'logvar': logvar,
'z': z
}
def compute_loss(self, x, d, y, outputs, beta=1.0, gamma=1.0):
"""
计算总损失:重构损失 + KL散度 + 因果一致性损失
Args:
beta: KL散度的权重
gamma: 因果损失的权重
"""
x_recon = outputs['x_recon']
d_prob = outputs['d_prob']
y_pred = outputs['y_pred']
mu = outputs['mu']
logvar = outputs['logvar']
batch_size = x.size(0)
# 1. 重构损失
recon_loss = F.mse_loss(x_recon, x, reduction='sum') / batch_size
# 2. KL散度损失
kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) / batch_size
# 3. 处理分配损失
d_loss = F.binary_cross_entropy(d_prob.squeeze(), d.squeeze(), reduction='mean')
# 4. 结果预测损失
y_loss = F.mse_loss(y_pred.squeeze(), y.squeeze(), reduction='mean')
# 总损失
total_loss = recon_loss + beta * kl_loss + gamma * (d_loss + y_loss)
return {
'total_loss': total_loss,
'recon_loss': recon_loss,
'kl_loss': kl_loss,
'd_loss': d_loss,
'y_loss': y_loss
}
class CounterfactualGenerator:
"""
反事实生成器:基于训练好的因果VAE生成反事实样本
实现Pearl的三步反事实推理算法
"""
def __init__(self, causal_vae_model):
self.model = causal_vae_model
self.model.eval()
def generate_counterfactuals(self, x_obs, d_obs, y_obs, d_counterfactual):
"""
生成反事实样本
Args:
x_obs: 观测到的特征
d_obs: 观测到的处理
y_obs: 观测到的结果
d_counterfactual: 反事实处理
Returns:
反事实特征、结果等
"""
with torch.no_grad():
# 第一步:溯因(Abduction) - 推断潜在变量
mu, logvar = self.model.encode(x_obs)
# 使用均值作为潜在变量的点估计(也可以采样)
z_inferred = mu
# 第二步:行动(Action) - 应用反事实干预
# 保持Z不变,改变处理变量D
# 第三步:预测(Prediction) - 计算反事实结果
# 生成反事实特征(理论上X不应该因为D的改变而改变,如果X是pre-treatment的话)
x_counterfactual = self.model.feature_decoder(z_inferred)
# 生成反事实结果
zd_counterfactual = torch.cat([z_inferred, d_counterfactual], dim=1)
y_counterfactual = self.model.outcome_model(zd_counterfactual)
# 计算个体处理效应 (ITE)
zd_treated = torch.cat([z_inferred, torch.ones_like(d_counterfactual)], dim=1)
zd_control = torch.cat([z_inferred, torch.zeros_like(d_counterfactual)], dim=1)
y_treated = self.model.outcome_model(zd_treated)
y_control = self.model.outcome_model(zd_control)
ite = y_treated - y_control
return {
'x_counterfactual': x_counterfactual,
'y_counterfactual': y_counterfactual,
'z_inferred': z_inferred,
'ite': ite,
'y_treated': y_treated,
'y_control': y_control
}
def explain_decision(self, x_obs, d_obs, y_obs):
"""
解释决策:回答"为什么会有这个结果?"
"""
# 生成两种反事实情况
d_alt = 1 - d_obs # 相反的处理
counterfactuals = self.generate_counterfactuals(x_obs, d_obs, y_obs, d_alt)
# 计算反事实差异
y_factual = y_obs
y_counterfactual = counterfactuals['y_counterfactual']
effect_of_treatment = y_factual - y_counterfactual
return {
'observed_outcome': y_factual,
'counterfactual_outcome': y_counterfactual,
'effect_of_treatment': effect_of_treatment,
'ite': counterfactuals['ite']
}
def generate_causal_data_with_latents(n_samples=2000, latent_dim=5, noise_level=0.1):
"""
生成具有潜在混淆变量的因果数据
真实的数据生成过程:Z -> X, Z -> D, Z+D -> Y
"""
np.random.seed(42)
# 1. 生成潜在混淆变量 Z
Z = np.random.randn(n_samples, latent_dim)
# 2. Z -> X (特征生成)
# 每个特征都受到不同潜在因子的影响
W_zx = np.random.randn(latent_dim, 10) * 0.5 # 权重矩阵
X = Z @ W_zx + np.random.randn(n_samples, 10) * noise_level
# 3. Z -> D (处理分配机制)
# 处理倾向受到前几个潜在因子的影响
propensity_score = 1 / (1 + np.exp(-(Z[:, :3].sum(axis=1) + 0.5)))
D = np.random.binomial(1, propensity_score).astype(float)
# 4. Z + D -> Y (结果生成机制)
# 结果同时受到潜在因子和处理的影响
base_outcome = Z[:, :3].sum(axis=1) + Z[:, 0] * Z[:, 1] # 非线性关系
treatment_effect = 1.5 + 0.5 * Z[:, 0] - 0.3 * Z[:, 1] # 异质性处理效应
Y = base_outcome + D * treatment_effect + np.random.randn(n_samples) * noise_level
# 转换为张量
X_tensor = torch.tensor(X, dtype=torch.float32)
D_tensor = torch.tensor(D.reshape(-1, 1), dtype=torch.float32)
Y_tensor = torch.tensor(Y.reshape(-1, 1), dtype=torch.float32)
Z_tensor = torch.tensor(Z, dtype=torch.float32)
# 真实的个体处理效应
true_ite = treatment_effect
return X_tensor, D_tensor, Y_tensor, Z_tensor, true_ite
def train_causal_vae():
"""训练因果VAE模型"""
print("🧠 开始训练因果变分自编码器...")
print("=" * 50)
# 1. 生成数据
print("📊 生成因果数据...")
X, D, Y, Z_true, true_ite = generate_causal_data_with_latents(n_samples=2000, latent_dim=5)
# 数据标准化
scaler_X = StandardScaler()
scaler_Y = StandardScaler()
X_scaled = torch.tensor(scaler_X.fit_transform(X.numpy()), dtype=torch.float32)
Y_scaled = torch.tensor(scaler_Y.fit_transform(Y.numpy()), dtype=torch.float32)
print(f"✅ 数据生成完成: {len(X)} 样本, {X.shape[1]} 特征")
# 2. 创建模型
model = CausalVAE(input_dim=X.shape[1], latent_dim=5, hidden_dim=64)
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)
# 3. 训练循环
print("\n🚀 开始训练...")
n_epochs = 200
batch_size = 64
```python
train_losses = []
for epoch in range(n_epochs):
epoch_losses = []
# 打乱数据
indices = torch.randperm(len(X_scaled))
for i in range(0, len(X_scaled), batch_size):
batch_indices = indices[i:i+batch_size]
x_batch = X_scaled[batch_indices]
d_batch = D[batch_indices]
y_batch = Y_scaled[batch_indices]
# 前向传播
outputs = model(x_batch, d_batch, y_batch)
# 计算损失
losses = model.compute_loss(x_batch, d_batch, y_batch, outputs,
beta=0.1, gamma=1.0)
# 反向传播
optimizer.zero_grad()
losses['total_loss'].backward()
optimizer.step()
epoch_losses.append(losses['total_loss'].item())
avg_loss = np.mean(epoch_losses)
train_losses.append(avg_loss)
if (epoch + 1) % 50 == 0:
print(f"Epoch {epoch+1}/{n_epochs}: Loss = {avg_loss:.4f}")
print("✅ 训练完成!")
# 4. 评估模型
print("\n📊 模型评估...")
model.eval()
with torch.no_grad():
outputs = model(X_scaled, D, Y_scaled)
# 评估重构质量
x_recon_loss = F.mse_loss(outputs['x_recon'], X_scaled).item()
y_pred_loss = F.mse_loss(outputs['y_pred'], Y_scaled).item()
print(f"特征重构MSE: {x_recon_loss:.4f}")
print(f"结果预测MSE: {y_pred_loss:.4f}")
# 5. 可视化训练过程
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(train_losses)
plt.title('训练损失')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.grid(True, alpha=0.3)
# 潜在空间可视化(取前两维)
plt.subplot(1, 2, 2)
with torch.no_grad():
mu, _ = model.encode(X_scaled)
z_learned = mu.numpy()
# 按处理组着色
colors = ['red' if d == 1 else 'blue' for d in D.squeeze()]
plt.scatter(z_learned[:, 0], z_learned[:, 1], c=colors, alpha=0.6, s=10)
plt.title('学习到的潜在表示')
plt.xlabel('Z1')
plt.ylabel('Z2')
plt.legend(['对照组', '处理组'])
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return model, scaler_X, scaler_Y, (X, D, Y, Z_true, true_ite)
def demonstrate_counterfactual_reasoning():
"""演示反事实推理的完整流程"""
print("\n🔮 反事实推理演示")
print("=" * 50)
# 1. 训练模型
model, scaler_X, scaler_Y, data = train_causal_vae()
X, D, Y, Z_true, true_ite = data
# 2. 创建反事实生成器
cf_generator = CounterfactualGenerator(model)
# 3. 选择几个测试样本
test_indices = [10, 50, 100, 200, 500]
print("\n🔍 反事实分析结果:")
print("-" * 70)
results = []
for idx in test_indices:
# 获取观测数据
x_obs = scaler_X.transform(X[idx:idx+1].numpy())
x_obs = torch.tensor(x_obs, dtype=torch.float32)
d_obs = D[idx:idx+1]
y_obs = Y[idx:idx+1]
# 生成反事实
d_cf = 1 - d_obs # 相反的处理
counterfactuals = cf_generator.generate_counterfactuals(x_obs, d_obs, y_obs, d_cf)
# 获取解释
explanation = cf_generator.explain_decision(x_obs, d_obs, y_obs)
# 反标准化结果
y_obs_original = scaler_Y.inverse_transform(y_obs.numpy())[0, 0]
y_cf_original = scaler_Y.inverse_transform(
counterfactuals['y_counterfactual'].numpy())[0, 0]
ite_estimated = scaler_Y.inverse_transform(
counterfactuals['ite'].numpy())[0, 0]
results.append({
'index': idx,
'observed_treatment': d_obs.item(),
'observed_outcome': y_obs_original,
'counterfactual_outcome': y_cf_original,
'estimated_ite': ite_estimated,
'true_ite': true_ite[idx]
})
print(f"样本 {idx:3d}: D={d_obs.item():.0f}, Y={y_obs_original:6.2f} | "
f"反事实Y={y_cf_original:6.2f} | ITE估计={ite_estimated:6.2f} | "
f"ITE真实={true_ite[idx]:6.2f}")
# 4. 整体性能评估
results_df = pd.DataFrame(results)
ite_mae = np.mean(np.abs(results_df['estimated_ite'] - results_df['true_ite']))
ite_rmse = np.sqrt(np.mean((results_df['estimated_ite'] - results_df['true_ite'])**2))
print(f"\n📊 ITE估计性能:")
print(f" 平均绝对误差 (MAE): {ite_mae:.4f}")
print(f" 均方根误差 (RMSE): {ite_rmse:.4f}")
# 5. 可视化反事实分析
plt.figure(figsize=(15, 5))
# ITE估计 vs 真实值
plt.subplot(1, 3, 1)
plt.scatter(results_df['true_ite'], results_df['estimated_ite'], alpha=0.7)
plt.plot([results_df['true_ite'].min(), results_df['true_ite'].max()],
[results_df['true_ite'].min(), results_df['true_ite'].max()],
'r--', label='完美预测')
plt.xlabel('真实ITE')
plt.ylabel('估计ITE')
plt.title('个体处理效应估计')
plt.legend()
plt.grid(True, alpha=0.3)
# 观测结果 vs 反事实结果
plt.subplot(1, 3, 2)
for _, row in results_df.iterrows():
color = 'red' if row['observed_treatment'] == 1 else 'blue'
plt.scatter(row['observed_outcome'], row['counterfactual_outcome'],
color=color, alpha=0.7, s=50)
plt.plot([row['observed_outcome'], row['counterfactual_outcome']],
[row['observed_outcome'], row['counterfactual_outcome']],
'k--', alpha=0.3)
plt.xlabel('观测结果')
plt.ylabel('反事实结果')
plt.title('观测 vs 反事实结果')
plt.grid(True, alpha=0.3)
# 处理效应分布
plt.subplot(1, 3, 3)
plt.hist(results_df['estimated_ite'], alpha=0.7, bins=10, label='估计ITE', density=True)
plt.hist(results_df['true_ite'], alpha=0.7, bins=10, label='真实ITE', density=True)
plt.xlabel('个体处理效应')
plt.ylabel('密度')
plt.title('ITE分布对比')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return cf_generator, results_df
# 运行完整演示
if __name__ == "__main__":
cf_generator, results = demonstrate_counterfactual_reasoning()
🎯 10. 因果推理的评估与验证:如何确保结果可靠?
在因果推理中,最大的挑战之一就是我们无法直接观测到反事实!这就像是在没有标准答案的情况下判断考试成绩一样困难。但是,聪明的研究者们发明了各种巧妙的方法来评估因果推理模型的性能。
10.1 因果推理评估的核心挑战
因果推理评估:从理论到实践
核心评估挑战
挑战 |
描述 |
传统ML的情况 |
因果推理的特殊性 |
基础真相缺失 |
无法观测反事实结果 |
有标签数据 |
反事实不可观测 |
分布偏移 |
训练和测试分布不同 |
IID假设 |
因果关系跨分布稳定 |
混淆变量 |
未观测因素影响结果 |
特征工程 |
因果识别问题 |
选择偏差 |
数据收集过程有偏 |
随机采样 |
观测数据有选择性 |
评估方法分类体系
类别 |
方法 |
适用场景 |
优势 |
局限性 |
仿真评估 |
合成数据 |
方法开发 |
已知真相 |
现实性有限 |
半仿真 |
真实数据+仿真处理 |
方法验证 |
部分现实性 |
处理机制简化 |
基准数据集 |
标准评估集 |
方法对比 |
标准化 |
数据集有限 |
实验验证 |
RCT实验 |
现实应用 |
金标准 |
成本高、伦理限制 |
评估指标体系
点估计指标
指标 |
公式 |
含义 |
适用场景 |
PEHE |
√E[(τ(x) - τ̂(x))²] |
异质性效应误差 |
个体效应评估 |
ATE误差 |
|
E[τ(x)] - E[τ̂(x)] |
|
ε-ATE |
P( |
τ̂ - τ |
< ε) |
分布评估指标
指标 |
描述 |
计算方法 |
应用价值 |
策略价值 |
基于CATE的决策收益 |
Σᵢ Y(1)ᵢ·I(τ̂(xᵢ)>0) |
决策应用评估 |
排序质量 |
高/低效应个体识别 |
AUC of τ̂ vs τ |
个性化策略 |
校准程度 |
预测不确定性准确性 |
可靠性图 |
风险控制 |
验证策略框架
内部验证 (Internal Validation)
复制代码
1. 交叉验证 → 模型稳定性
2. 敏感性分析 → 假设稳健性
3. 残差分析 → 模型适配性
4. 特征重要性 → 可解释性
外部验证 (External Validation)
复制代码
1. 时间分割 → 时间泛化性
2. 地域分割 → 空间泛化性
3. 人群分割 → 群体泛化性
4. 场景分割 → 应用泛化性
常见陷阱与解决方案
陷阱 |
表现 |
原因 |
解决方案 |
过拟合偏差 |
训练表现好,应用差 |
模型复杂度过高 |
正则化、交叉验证 |
Simpson悖论 |
分层分析矛盾 |
混淆变量作祟 |
因果图分析 |
幸存者偏差 |
样本代表性差 |
数据收集有选择性 |
敏感性分析 |
后视偏差 |
结果影响解释 |
知道结果后解释 |
预注册分析计划 |
最佳实践清单
✅ 数据质量
✅ 模型验证
✅ 结果解释
10.2 PyTorch实现综合评估框架
让我们构建一个完整的因果推理评估框架,它能够自动化地测试不同方法的性能:
python
复制代码
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.linear_model import LinearRegression
from sklearn.metrics import roc_auc_score, mean_squared_error
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
class CausalEvaluationFramework:
"""
因果推理综合评估框架
支持多种评估指标和验证策略
"""
def __init__(self, random_state=42):
self.random_state = random_state
self.results = {}
np.random.seed(random_state)
def pehe_score(self, true_ite, pred_ite):
"""
精确异质性效应误差 (Precision in Estimation of Heterogeneous Effect)
衡量个体处理效应估计的精度
"""
return np.sqrt(np.mean((true_ite - pred_ite) ** 2))
def ate_error(self, true_ite, pred_ite):
"""平均处理效应误差"""
true_ate = np.mean(true_ite)
pred_ate = np.mean(pred_ite)
return abs(true_ate - pred_ate)
def policy_value(self, true_ite, pred_ite, y1, y0):
"""
策略价值:基于预测的CATE进行决策的平均收益
如果预测效应为正就给予处理,否则不给予
"""
# 基于预测效应的决策
policy_decisions = (pred_ite > 0).astype(int)
# 计算每个个体在该策略下的期望收益
policy_value = np.mean(policy_decisions * y1 + (1 - policy_decisions) * y0)
return policy_value
def oracle_policy_value(self, true_ite, y1, y0):
"""理想策略的价值(使用真实ITE)"""
optimal_decisions = (true_ite > 0).astype(int)
return np.mean(optimal_decisions * y1 + (1 - optimal_decisions) * y0)
def policy_risk(self, true_ite, pred_ite):
"""
策略风险:错误决策的比例
"""
true_decisions = (true_ite > 0).astype(int)
pred_decisions = (pred_ite > 0).astype(int)
return np.mean(true_decisions != pred_decisions)
def tau_risk(self, true_ite, pred_ite):
"""τ-risk: 处理效应估计的分位数风险"""
errors = np.abs(true_ite - pred_ite)
# 计算不同分位数的风险
quantiles = [0.5, 0.75, 0.9, 0.95]
risks = {f"τ-risk-{q}": np.quantile(errors, q) for q in quantiles}
return risks
def correlation_metrics(self, true_ite, pred_ite):
"""相关性指标"""
pearson_r, _ = stats.pearsonr(true_ite, pred_ite)
spearman_r, _ = stats.spearmanr(true_ite, pred_ite)
return {
'pearson_correlation': pearson_r,
'spearman_correlation': spearman_r
}
def calibration_metrics(self, true_ite, pred_ite, pred_std=None):
"""校准指标:预测不确定性是否准确"""
if pred_std is None:
return {'calibration_error': np.nan}
# 将预测分为几个置信区间
confidence_levels = [0.68, 0.95] # 1σ, 2σ
calibration_errors = {}
for level in confidence_levels:
z_score = stats.norm.ppf((1 + level) / 2)
lower = pred_ite - z_score * pred_std
upper = pred_ite + z_score * pred_std
# 计算实际覆盖率
actual_coverage = np.mean((true_ite >= lower) & (true_ite <= upper))
calibration_error = abs(actual_coverage - level)
calibration_errors[f'calibration_error_{level}'] = calibration_error
return calibration_errors
def comprehensive_evaluation(self, methods_results, true_data):
"""
综合评估多个方法
Args:
methods_results: dict, {method_name: {'ite': pred_ite, 'std': pred_std}}
true_data: dict, {'ite': true_ite, 'y1': y1, 'y0': y0}
"""
true_ite = true_data['ite']
y1 = true_data['y1']
y0 = true_data['y0']
evaluation_results = {}
# 计算理想策略价值作为基准
oracle_value = self.oracle_policy_value(true_ite, y1, y0)
for method_name, predictions in methods_results.items():
pred_ite = predictions['ite']
pred_std = predictions.get('std', None)
# 基础指标
metrics = {
'pehe': self.pehe_score(true_ite, pred_ite),
'ate_error': self.ate_error(true_ite, pred_ite),
'policy_value': self.policy_value(true_ite, pred_ite, y1, y0),
'policy_risk': self.policy_risk(true_ite, pred_ite),
'rmse': np.sqrt(mean_squared_error(true_ite, pred_ite)),
'mae': np.mean(np.abs(true_ite - pred_ite))
}
# 策略价值相对于理想策略的比例
metrics['policy_value_ratio'] = metrics['policy_value'] / oracle_value
# τ-risk指标
tau_risks = self.tau_risk(true_ite, pred_ite)
metrics.update(tau_risks)
# 相关性指标
corr_metrics = self.correlation_metrics(true_ite, pred_ite)
metrics.update(corr_metrics)
# 校准指标
if pred_std is not None:
calib_metrics = self.calibration_metrics(true_ite, pred_ite, pred_std)
metrics.update(calib_metrics)
evaluation_results[method_name] = metrics
self.results = evaluation_results
return evaluation_results
def create_evaluation_report(self, save_path=None):
"""生成详细的评估报告"""
if not self.results:
raise ValueError("请先运行comprehensive_evaluation方法")
# 创建结果DataFrame
df = pd.DataFrame(self.results).T
# 排序(按PEHE分数,越小越好)
df_sorted = df.sort_values('pehe')
print("🏆 因果推理方法性能排行榜")
print("=" * 60)
# 显示主要指标
main_metrics = ['pehe', 'ate_error', 'policy_value_ratio', 'rmse', 'pearson_correlation']
print("\n📊 主要性能指标:")
print("-" * 60)
for metric in main_metrics:
if metric in df.columns:
print(f"{metric:20s} | 最佳: {df[metric].min():.4f} | 最差: {df[metric].max():.4f}")
print(f"\n🥇 各指标最佳方法:")
print("-" * 60)
for metric in main_metrics:
if metric in df.columns:
if metric in ['pehe', 'ate_error', 'rmse', 'mae']: # 越小越好
best_method = df[metric].idxmin()
best_value = df[metric].min()
else: # 越大越好
best_method = df[metric].idxmax()
best_value = df[metric].max()
print(f"{metric:20s}: {best_method:15s} ({best_value:.4f})")
# 可视化结果
self.visualize_results(df_sorted)
if save_path:
df_sorted.to_csv(save_path)
print(f"\n💾 结果已保存到: {save_path}")
return df_sorted
def visualize_results(self, results_df):
"""可视化评估结果"""
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
# 1. PEHE分数比较
axes[0, 0].bar(range(len(results_df)), results_df['pehe'], color='skyblue')
axes[0, 0].set_xticks(range(len(results_df)))
axes[0, 0].set_xticklabels(results_df.index, rotation=45)
axes[0, 0].set_title('PEHE分数 (越小越好)')
axes[0, 0].set_ylabel('PEHE')
# 2. ATE误差比较
axes[0, 1].bar(range(len(results_df)), results_df['ate_error'], color='lightcoral')
axes[0, 1].set_xticks(range(len(results_df)))
axes[0, 1].set_xticklabels(results_df.index, rotation=45)
axes[0, 1].set_title('ATE误差 (越小越好)')
axes[0, 1].set_ylabel('ATE Error')
# 3. 策略价值比例
if 'policy_value_ratio' in results_df.columns:
axes[0, 2].bar(range(len(results_df)), results_df['policy_value_ratio'], color='lightgreen')
axes[0, 2].axhline(y=1.0, color='red', linestyle='--', label='理想策略')
axes[0, 2].set_xticks(range(len(results_df)))
axes[0, 2].set_xticklabels(results_df.index, rotation=45)
axes[0, 2].set_title('策略价值比例 (越接近1越好)')
axes[0, 2].set_ylabel('Policy Value Ratio')
axes[0, 2].legend()
# 4. 相关性比较
if 'pearson_correlation' in results_df.columns:
axes[1, 0].bar(range(len(results_df)), results_df['pearson_correlation'], color='gold')
axes[1, 0].set_xticks(range(len(results_df)))
axes[1, 0].set_xticklabels(results_df.index, rotation=45)
axes[1, 0].set_title('Pearson相关性 (越大越好)')
axes[1, 0].set_ylabel('Correlation')
# 5. 多指标雷达图
metrics_to_plot = ['pehe', 'ate_error', 'rmse', 'policy_risk']
if all(m in results_df.columns for m in metrics_to_plot):
# 标准化指标(都转换为越大越好)
normalized_data = results_df[metrics_to_plot].copy()
for col in normalized_data.columns:
# 对于越小越好的指标,取倒数并标准化
normalized_data[col] = 1 / (1 + normalized_data[col])
# 绘制多方法比较
axes[1, 1].bar(range(len(metrics_to_plot)),
normalized_data.iloc[0],
alpha=0.7, label=normalized_data.index[0])
if len(normalized_data) > 1:
axes[1, 1].bar(range(len(metrics_to_plot)),
normalized_data.iloc[1],
alpha=0.7, label=normalized_data.index[1])
axes[1, 1].set_xticks(range(len(metrics_to_plot)))
axes[1, 1].set_xticklabels(metrics_to_plot, rotation=45)
axes[1, 1].set_title('综合性能对比')
axes[1, 1].legend()
# 6. 性能排名
performance_score = results_df['pehe'].rank() + results_df['ate_error'].rank()
axes[1, 2].barh(range(len(results_df)), performance_score, color='purple', alpha=0.7)
axes[1, 2].set_yticks(range(len(results_df)))
axes[1, 2].set_yticklabels(results_df.index)
axes[1, 2].set_title('综合排名 (越小越好)')
axes[1, 2].set_xlabel('排名分数')
plt.tight_layout()
plt.show()
def generate_benchmark_data(n_samples=1500, scenario='nonlinear'):
"""
生成基准评估数据
支持多种数据生成场景
"""
np.random.seed(42)
if scenario == 'linear':
# 线性场景:简单的线性关系
X = np.random.randn(n_samples, 5)
# 简单的处理分配
propensity = 1 / (1 + np.exp(-(X[:, 0] + 0.3 * X[:, 1])))
D = np.random.binomial(1, propensity)
# 线性的异质性效应
true_ite = 1.0 + 0.5 * X[:, 0] - 0.3 * X[:, 1]
# 结果生成
y0 = X.sum(axis=1) + np.random.normal(0, 0.5, n_samples)
y1 = y0 + true_ite
elif scenario == 'nonlinear':
# 非线性场景:复杂的非线性关系
X = np.random.randn(n_samples, 8)
# 复杂的处理分配
propensity_logit = (X[:, 0] + 0.5 * X[:, 1] - 0.3 * X[:, 2] +
0.2 * X[:, 0] * X[:, 1] + 0.1 * X[:, 0]**2)
propensity = 1 / (1 + np.exp(-propensity_logit))
D = np.random.binomial(1, propensity)
# 非线性的异质性效应
true_ite = (1.5 + X[:, 0] - 0.5 * X[:, 1] +
0.3 * X[:, 0] * X[:, 1] + 0.2 * np.sin(X[:, 2]) +
0.1 * X[:, 3]**2)
# 复杂的结果生成
base_outcome = (X[:, :4].sum(axis=1) + 0.3 * X[:, 0] * X[:, 2] +
0.2 * np.exp(X[:, 1]/3) + 0.1 * X[:, 3]**2)
y0 = base_outcome + np.random.normal(0, 0.5, n_samples)
y1 = y0 + true_ite
else: # scenario == 'confounded'
# 强混淆场景:存在强混淆变量
X = np.random.randn(n_samples, 6)
# 强混淆变量影响处理和结果
confounding_strength = 2.0
propensity_logit = confounding_strength * (X[:, 0] + X[:, 1])
propensity = 1 / (1 + np.exp(-propensity_logit))
D = np.random.binomial(1, propensity)
# 异质性效应
true_ite = 1.0 + 0.8 * X[:, 0] - 0.4 * X[:, 1]
# 混淆变量同时影响结果
confounded_outcome = confounding_strength * (X[:, 0] + X[:, 1])
y0 = confounded_outcome + X[:, 2:].sum(axis=1) + np.random.normal(0, 0.5, n_samples)
y1 = y0 + true_ite
# 生成观测结果
Y = D * y1 + (1 - D) * y0
return {
'X': X,
'D': D,
'Y': Y,
'true_ite': true_ite,
'y0': y0,
'y1': y1
}
def run_comprehensive_evaluation():
"""运行综合评估实验"""
print("🎯 启动因果推理综合评估实验")
print("=" * 60)
# 1. 生成测试数据
scenarios = ['linear', 'nonlinear', 'confounded']
all_results = {}
for scenario in scenarios:
print(f"\n📊 评估场景: {scenario}")
print("-" * 40)
data = generate_benchmark_data(n_samples=1500, scenario=scenario)
X, D, Y = data['X'], data['D'], data['Y']
true_ite, y0, y1 = data['true_ite'], data['y0'], data['y1']
# 2. 训练不同的方法
methods_results = {}
# 方法1: S-Learner (Random Forest)
print("🌲 训练S-Learner...")
s_learner = RandomForestRegressor(n_estimators=100, random_state=42)
X_with_D = np.column_stack([X, D])
s_learner.fit(X_with_D, Y)
X_with_1 = np.column_stack([X, np.ones(len(X))])
X_with_0 = np.column_stack([X, np.zeros(len(X))])
s_pred_1 = s_learner.predict(X_with_1)
s_pred_0 = s_learner.predict(X_with_0)
s_ite = s_pred_1 - s_pred_0
methods_results['S-Learner'] = {'ite': s_ite}
# 方法2: T-Learner (分别训练处理组和对照组)
print("🎯 训练T-Learner...")
t_learner_1 = RandomForestRegressor(n_estimators=100, random_state=42)
t_learner_0 = RandomForestRegressor(n_estimators=100, random_state=42)
treated_mask = D == 1
control_mask = D == 0
t_learner_1.fit(X[treated_mask], Y[treated_mask])
t_learner_0.fit(X[control_mask], Y[control_mask])
t_pred_1 = t_learner_1.predict(X)
t_pred_0 = t_learner_0.predict(X)
t_ite = t_pred_1 - t_pred_0
methods_results['T-Learner'] = {'ite': t_ite}
# 方法3: X-Learner
print("❌ 训练X-Learner...")
# 第一阶段:估计μ₀和μ₁
mu_0 = RandomForestRegressor(n_estimators=50, random_state=42)
mu_1 = RandomForestRegressor(n_estimators=50, random_state=42)
mu_0.fit(X[control_mask], Y[control_mask])
mu_1.fit(X[treated_mask], Y[treated_mask])
# 第二阶段:估计τ
# 对处理组:τ = Y - μ₀(X)
tau_treated = Y[treated_mask] - mu_0.predict(X[treated_mask])
# 对对照组:τ = μ₁(X) - Y
tau_control = mu_1.predict(X[control_mask]) - Y[control_mask]
# 训练τ模型
tau_1_learner = RandomForestRegressor(n_estimators=50, random_state=42)
tau_0_learner = RandomForestRegressor(n_estimators=50, random_state=42)
tau_1_learner.fit(X[treated_mask], tau_treated)
tau_0_learner.fit(X[control_mask], tau_control)
# 最终预测:加权平均
propensity_model = GradientBoostingRegressor(n_estimators=50, random_state=42)
propensity_model.fit(X, D)
e_x = propensity_model.predict(X)
e_x = np.clip(e_x, 0.01, 0.99) # 避免极端值
x_ite = e_x * tau_0_learner.predict(X) + (1 - e_x) * tau_1_learner.predict(X)
methods_results['X-Learner'] = {'ite': x_ite}
# 方法4: 朴素方法(直接差值)
naive_ite = np.full(len(X), Y[treated_mask].mean() - Y[control_mask].mean())
methods_results['Naive-ATE'] = {'ite': naive_ite}
# 3. 评估所有方法
evaluator = CausalEvaluationFramework()
true_data = {
'ite': true_ite,
'y0': y0,
'y1': y1
}
eval_results = evaluator.comprehensive_evaluation(methods_results, true_data)
# 4. 生成评估报告
results_df = evaluator.create_evaluation_report()
all_results[scenario] = {
'evaluator': evaluator,
'results_df': results_df,
'eval_results': eval_results
}
return all_results
# 运行综合评估
if __name__ == "__main__":
evaluation_results = run_comprehensive_evaluation()
# 汇总不同场景的结果
print("\n🏆 各场景最佳方法汇总:")
print("=" * 50)
for scenario, results in evaluation_results.items():
best_method = results['results_df'].index[0] # 按PEHE排序的第一个
best_pehe = results['results_df'].iloc[0]['pehe']
print(f"{scenario:12s}: {best_method:15s} (PEHE: {best_pehe:.4f})")
🚀 11. 大规模因果推理的工程实践
在实际业务场景中,我们经常需要处理数百万甚至数十亿的数据!传统的因果推理方法在这种规模下往往力不从心。这就需要我们掌握大规模因果推理的工程技巧------既要保证统计的严谨性,又要确保计算的可扩展性。
大规模因果推理:从实验室到生产环境
系统架构设计
组件层级 |
技术选型 |
主要功能 |
性能考量 |
数据接入层 |
Kafka + Flink |
实时数据流处理 |
吞吐量: >100K events/s |
存储层 |
HDFS + Delta Lake |
历史数据存储 |
存储: PB级别 |
计算层 |
Spark + PyTorch |
分布式模型训练 |
计算: 1000+ GPU |
服务层 |
Flask + Redis |
在线推理服务 |
延迟: <50ms |
监控层 |
Prometheus + Grafana |
系统监控 |
可观测性 |
分布式训练策略
数据并行 vs 模型并行
策略 |
适用场景 |
优势 |
挑战 |
PyTorch实现 |
数据并行 |
大数据量,中等模型 |
实现简单,扩展性好 |
通信开销 |
DistributedDataParallel |
模型并行 |
超大模型,中等数据 |
内存友好 |
复杂度高 |
手动分割 |
流水线并行 |
超大模型+大数据 |
资源利用率高 |
调度复杂 |
PipeDream |
梯度压缩 |
带宽受限环境 |
通信效率高 |
精度损失 |
Horovod |
内存优化技术
技术 |
原理 |
内存节省 |
代码示例 |
梯度检查点 |
重计算换内存 |
50-80% |
torch.utils.checkpoint |
混合精度 |
FP16+FP32训练 |
30-50% |
torch.cuda.amp |
模型分片 |
参数分布存储 |
线性扩展 |
FairScale.FSDP |
动态内存 |
按需分配释放 |
10-20% |
torch.cuda.empty_cache() |
计算优化策略
批处理优化
python
复制代码
# 传统单样本处理
for x, d, y in dataset:
effect = model.predict_ite(x, d, y) # 低效
# 批量向量化处理
batch_effects = model.predict_ite_batch(X_batch, D_batch, Y_batch) # 高效
近似算法
算法 |
精度损失 |
速度提升 |
适用场景 |
随机森林裁剪 |
<5% |
3-5x |
因果森林 |
梯度近似 |
<10% |
5-10x |
双重ML |
采样估计 |
<15% |
10-50x |
大规模ATE |
在线推理优化
模型服务化
python
复制代码
# 模型缓存策略
class CausalModelService:
def __init__(self):
self.model_cache = {} # LRU缓存
self.feature_cache = {} # 特征缓存
@lru_cache(maxsize=1000)
def predict_ite(self, user_id, treatment):
# 缓存用户特征和预测结果
return self._compute_ite(user_id, treatment)
延迟优化技术
技术 |
延迟减少 |
实现复杂度 |
适用场景 |
模型蒸馏 |
60-80% |
中等 |
实时推荐 |
特征预计算 |
40-60% |
简单 |
批量处理 |
异步推理 |
20-40% |
高 |
非实时场景 |
边缘计算 |
50-70% |
高 |
移动应用 |
数据流架构
Lambda架构
复制代码
实时流: Kafka → Flink → 在线特征存储 → 实时推理
批处理: HDFS → Spark → 离线特征存储 → 批量训练
合并层: 实时+批处理结果融合
Kappa架构
复制代码
统一流: Kafka → Flink → 统一存储 → 流式训练+推理
优势: 架构简单,一致性好
适用: 实时性要求高的场景
性能基准测试
数据规模 |
传统方法 |
分布式优化 |
加速比 |
10万样本 |
5分钟 |
30秒 |
10x |
100万样本 |
2小时 |
8分钟 |
15x |
1000万样本 |
1天 |
45分钟 |
32x |
1亿样本 |
1周 |
4小时 |
42x |
故障容错机制
故障类型 |
检测方法 |
恢复策略 |
预防措施 |
节点故障 |
心跳检测 |
任务重调度 |
多副本 |
数据损坏 |
校验和 |
备份恢复 |
数据校验 |
网络分区 |
超时检测 |
降级服务 |
多机房 |
内存溢出 |
资源监控 |
自动重启 |
内存限制 |
成本优化策略
资源调度优化
python
复制代码
# 动态资源分配
class ResourceScheduler:
def allocate_resources(self, workload_type):
if workload_type == 'training':
return {'gpu': 8, 'memory': '64GB', 'cpu': 32}
elif workload_type == 'inference':
return {'gpu': 1, 'memory': '8GB', 'cpu': 4}
else:
return self.get_minimal_resources()
成本控制指标
指标 |
目标值 |
监控方法 |
优化策略 |
GPU利用率 |
>80% |
CUDA监控 |
任务调度优化 |
存储成本 |
<$0.1/GB/月 |
成本分析 |
数据生命周期管理 |
网络带宽 |
<10% total |
流量监控 |
压缩+缓存 |
计算成本 |
<$1/万次推理 |
成本追踪 |
算法优化 |
11.1 分布式因果推理架构### 11.2 前沿应用案例:因果推理改变世界
让我们看看因果推理在各个领域的前沿应用,这些案例展示了因果推理的巨大潜力:
🎯 第二部分总结:高级因果推理的掌握之道
经过这一部分深入而全面的学习,我们已经从因果推理的"新手村"成功进阶到了"高级玩家"的水平!让我用一个系统性的框架来总结我们在第二部分中获得的核心能力和深刻洞察。
怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!