← 返回蜂巢洞察

用于估算人工智能提示设计效果的产品实验反事实方法

想象一下,你的团队在两周前将A提示在全球范围内推广使用。由于时间紧迫且对效果充满信心,因此在没有进行任何A/B测试、也没有设置测试对照组或保留用户群的情况下,这一更新就应用到了100%的用户身上。 虽然各项完成率看起来都很稳定,但深夜时一位同事从测试环境中提交了一个新的提示方案,这就引发了一个重要的问题:这个替代方案是否才是更合适的上线选择呢? 现在你陷入了这些已记录的数据所构成的困境之中。这个问题看似无解,但实际上并非如此。产品团队会通过前瞻性实验来观察如果推出某个功能会发生什么结果;而反事实估算则能帮助我们了解:如果选择了其他方案,最终会得到什么样的结果。 对于那些负责处理大语言模型产品日

想象一下,你的团队在两周前将A提示在全球范围内推广使用。由于时间紧迫且对效果充满信心,因此在没有进行任何A/B测试、也没有设置测试对照组或保留用户群的情况下,这一更新就应用到了100%的用户身上。

虽然各项完成率看起来都很稳定,但深夜时一位同事从测试环境中提交了一个新的提示方案,这就引发了一个重要的问题:这个替代方案是否才是更合适的上线选择呢?

现在你陷入了这些已记录的数据所构成的困境之中。这个问题看似无解,但实际上并非如此。产品团队会通过前瞻性实验来观察如果推出某个功能会发生什么结果;而反事实估算则能帮助我们了解:如果选择了其他方案,最终会得到什么样的结果。

对于那些负责处理大语言模型产品日志的数据科学和产品工程领域的专家来说,一旦某个提示方案正式投入生产,这种反事实估算方法往往就是唯一可行的分析途径。你手头所有的数据都来自使用了A提示的用户,因此这些分析纯粹属于事后回顾性质。你无法回到过去,重新运行实验并改变实验条件——这就是典型的反事实问题。

团队们通常会迅速推出新的提示方案,然后收集相关日志,接着再对这些数据进行事后分析:如果使用不同的系统提示方案,转化率会有什么变化?哪些用户的行为会因此有所不同?新模型带来的效果真的是由于该提示方案的改进,还是同时部署的其他因素共同作用的结果呢?

解决这些问题的方法就是利用所谓的“反事实估算”技术,尤其是那些基于元学习器的算法。其核心思想就是利用现有数据中的各种差异来构建模型,从而预测在任何特定条件下,任何个别用户会遇到什么样的结果。这些差异可能来源于那些接受了不同提示方案、经历了不同的路由决策或使用了不同功能版本的用户。

在本指南中,你将使用scikit-learn从零开始实现T学习器和X学习器。你会学习如何计算自助法置信区间,并将这些估算结果转化为具体的决策依据。你还会了解到:如果能够将每个用户分配到被认为最能帮助他们的那款提示方案下,整体的效果会有怎样的提升。

本教程中的每一个代码示例都可以在以下链接对应的笔记本文件中完整运行:product-experimentation-causal-inference-genai-llm/tree/main/10_counterfactual/prompts/。该笔记本文件的名称为counterfactual_demo.ipynb

目录

为什么记录的数据不能被视为实验结果

记录的生产数据存在一个根本性问题:分配给用户的处理方案很少是随机的。在随机化的A/B测试中,决定用户使用提示A还是提示B的机制与其他所有因素都是无关的。两组用户在参与程度、查询类型和会话时长等方面具有完全相同的分布情况,包括那些你尚未测量到的、未被观察到的特征。

两组之间唯一的系统性差异就是所施加的处理方案本身,因此任何结果上的差异都必然是这种处理方案的因果效应所致。

然而生产日志的数据结构却有所不同。用户最终看到某个特定提示,背后往往有各种原因:他们所处的工作环境、被分配到的功能模块、发送查询的时间,或者他们使用的是哪个版本的模型等等。

其中一些原因会被记录在数据中,但其余的则隐藏了起来。当你根据日志计算看到提示A的用户与看到提示B的用户之间的结果差异时,你实际上同时吸收了这两个组之间所有的系统性差异,以及这些差异所蕴含的因果信息。

问题就出在这里:在本教程所描述的场景中,记录的数据其实包含了随机分配的处理方案。

假设这种情况并不存在吧。如果提示B恰好被分配给了那些更频繁使用该产品、发送更复杂查询、且订阅时间更长的用户,那么单纯比较这两种处理方案的效果,就会大大高估提示B的实际效果。

针对这种非随机情况,专门设计了反事实估计方法。本指南中的实现方式同样适用,无论最初的分配方案是否是随机的,还是存在其他混淆因素的。

你真正关心的是:用户实际体验到了什么,以及如果采用不同的处理方案,他们本来会体验到什么。反事实估计方法能够为这两种情况分别生成预测结果,尽管每个用户实际上只经历了一种处理方案。

反事实估计的原理

f1b9cc06-8387-45ba-9e1f-e55baac41cbe

图1:T学习器的概念示意图。蓝色曲线(m0)表示在提示A下的任务完成情况,红色曲线(m1)则表示在提示B下的完成情况。两者之间的绿色阴影区域代表在每个查询置信度值下CATE的值。下方面板展示了CATE如何随协变量范围的变化而变化,其中真实效果+4个标准差的参考线也被标了出来。

潜在结果框架(Rubin, 1974; Holland, 1986)为分析这个问题提供了最清晰的方法。对于每个用户i来说,用\(Y_i(1)\)表示他们在提示B下的结果,用\(Y_i(0)\)表示在提示A下的结果。你真正关心的是他们的个体处理效应:\(\tau_i = Y_i(1) - Y_i(0)\)。

根本问题在于,你实际上只能观察到两种结果中的一种。看到提示A的用户会提供你\(Y_i(0)\)这个数据,而\(Y_i(1)\)则无法被获取;看到提示B的用户则会提供你\(Y_i(1)\),而\(Y_i(0)\)同样无法被获取。

仅凭单一观察结果是无法确定个体处理效应的。因此,你可以估算条件平均处理效应(CATE):\(\tau(x) = E[Y(1) - Y(0) \mid X = x]\)。

这个数值表示具有特定协变量特征\(x\)的用户所期望得到的处理效应。通过将每种处理方式下条件均值结果作为协变量的函数来建立模型,你就可以预测任何用户的“反事实”均值,然后计算两者之间的差异。这个预测出的差异值就是该个体的CATE估计值。

这种方法需要满足两个主要假设。第一个假设是“无混杂性”:在观察到了所有协变量之后,处理分配的结果应该与随机分配的结果相同。用公式表示就是\((Y(0), Y(1)) \perp T \mid X\)。

如果存在未被观测到的变量同时影响了用户看到的提示类型以及他们完成任务的情况,那么这个假设就不成立了,从而导致偏差的出现。

第二个假设是“正性假设”,也就是所有用户都有一定的概率接受其中任何一种处理方式。如果某些用户群体只看到了其中一种提示,那么就无法为他们进行反事实预测了。

第三个假设是“稳定单元处理值假设”(SUTVA),它意味着每个用户的潜在结果仅取决于他们所接受的处理方式,其他用户收到的提示并不会影响他们的最终结果。

在单租户模式的SaaS产品中,这一假设是相当合理的,因为这些产品中用户的任务完成情况通常是相互独立的。但在协作式工作环境中,如果某个用户使用某种提示,就可能会影响到整个团队的行为,因此这个假设就不那么适用了。

“元学习器”是一类利用标准监督学习模型来估算CATE的工具。它们允许你使用像scikit-learn这样的熟悉工具来处理已有的数据。这些工具预测结果之间的差异值就是你所需要的反事实估计值。本教程的全部内容都是围绕这个主题展开的。

先决条件与准备工作

要跟随本教程进行学习,你需要准备以下条件:

  • Python 3.11或更高版本

  • 熟悉pandas和scikit-learn库

  • 有因果推断方面的经验会更有帮助,但即使没有这些经验也能学习本教程

请安装本教程所需的软件包:

pip install numpy pandas scikit-learn

请克隆配套代码仓库以获取合成数据集:

git clone https://github.com/RudrenduPaul/product-experimentation-causal-inference-genai-llm.git
cd product-experimentation-causal-inference-genai-llm
python data/generate_data.py --seed 42 --n-users 50000 --out data/synthetic_llm_logs.csv
该数据集模拟了一个具有两种提示方式的SaaS产品。其中,提示A属于对照组,而提示B属于实验组。数据集中包含50,000名用户,这些用户被平均分配到这两个组中。评估结果采用二进制指标来表示任务是否完成,而相关的协变量包括用户的参与程度以及他们对查询结果的信心水平。

该数据生成器预设了一个总体为+4个百分点的因果效应,这意味着你可以用已知的答案来验证这些估算结果。在实际生产环境中,这种便利是很少能获得的。

请加载数据,了解你正在处理的是什么信息:

import pandas as pd
import numpy as np

df = pd.read_csv("data/synthetic_llm_logs.csv")

print("数据结构:", df.shape)
print("\n实验组规模分布:")
print(df.prompt_variant.value_counts().to_dict())

print("\n不同提示变体下的任务完成情况:

print(df.groupby("promptARIANT").task_completed.agg(["mean", "count")).round(4)) naive_effect = ( df[df.prompt VARIANT == 1].task_completed.mean() - df[df.prompt_variant == 0].task_completed.mean() ) print(f"\n朴素差异:{naive_effect:+.4f}")

预期输出结果:

数据结构:(50000, 16)

实验组规模分布:
{0: 25000, 1: 25000}

不同提示变体下的任务完成情况:
              mean  count
prompt_variant
0             0.60  25000
1             0.63  25000

朴素差异: +0.0260

两种实验组在任务完成率上的差异约为+0.026。接下来,需要为机器学习模型构建特征矩阵:

X_cols = ["engagement_tier", "query_confidence"]
X = pd.get_dummies(df[Xcols], drop_first=True).astype(float)
X_arr = X.values

treatment = df["prompt_variant"].values
outcome = df["task_completed"].values

print("特征矩阵的结构:", X_arr.shape)
print("特征名称列表:", list(X.columns))
print("实验组分配的平衡性:", treatment.mean().round(4))

预期输出结果:

特征矩阵的结构:(50000, 2)
特征名称列表:['engagement_tier_light', 'engagement_tier_medium']
实验组分配的平衡性: 0.5000

具体操作过程如下:将engagement_tier进行独热编码(删除参考类别以避免相关性问题),而将query_confidence保留为连续型浮点数,然后将其转换成numpy数组以便后续的sklearn模型使用。还需要确认实验组之间的分配是否平衡——在这个数据集中,实验组的分布比例大致为50/50。在观察性研究中,如果实验组分配不平衡,这很可能是存在混杂因素的信号。

a3c3f8c7-df82-450e-928e-5bfc24c2541d

图2:在包含50,000个用户的合成数据集上,T-learner模型根据用户参与程度划分的CATE分布情况。重度用户(红色,平均CATE约为+0.048)从提示B中获得的收益明显高于轻度用户(蓝色,平均CATE约为+0.053)或中度用户(棕褐色,平均CATE约为+0.031)。下方面板显示了各层次用户的平均CATE值与整体平均值的对比情况(虚线表示)。与图1不同,这些估算结果是基于真实合成数据通过T-learner模型计算得出的,而非基于理论模型推导得出的。

步骤1:用于反事实预测的T学习器

T学习器(Künzel等人,2019年)是一种最为直接的元学习器。你需要构建两个完全独立的模型:一个用于处理组的数据,另一个用于对照组的数据。对于任何用户而言,反事实预测结果都是通过使用相反处理组数据训练得到的模型来计算得出的。

from sklearn.linear_model import LogisticRegression

# 为每个处理组分别构建预测模型
m0 = LogisticRegression(max_iter=1000)
m1 = LogisticRegression(max_iter=1000)

m0.fit(X_arr[treatment == 0], outcome[treatment == 0])
m1.fit(X_arr[treatment == 1], outcome[treatment == 1])

# 预测每个用户在两种提示下的可能结果
mu0 = m0.predict_proba(X_arr)[:, 1]   # 根据提示A预测的结果概率
mu1 = m1.predict_proba(X_arr)[:, 1]   # 根据提示B预测的结果概率

# 计算个体层面的差异
cate_t = mu1 - mu0

print(f"T学习器计算出的平均个体差异:{cate_t.mean():+.4f}")
print(f"T学习器计算出的个体差异标准差:{cate_t.std():.4f}")
print(f"个体差异的范围:[{cate_t.min():.4f}, {cate_t.max():.4f}]")

print("\n按参与程度划分的平均个体差异:")
df["cate_t"] = cate_t
print(df.groupby("engagement_tier").cate_t.mean().round(4))

预期输出:

T学习器计算出的平均个体差异:+0.0260
T学习器计算出的个体差异标准差:0.0100

按参与程度划分的平均个体差异:
参与程度
高参与度    0.0400
低参与度    0.0300
中等参与度 0.0130
Name: cate_t, dtype: float64

具体运作原理如下:m0模型专门针对看到提示A的用户,学习用户特征与任务完成结果之间的关系;m1模型则专门针对看到提示B的用户学习这一关系。

对于数据集中的每个用户,无论他们实际看到了哪种提示,我们都会使用这两个模型来预测他们的结果:在提示A下的结果以及在提示B下的结果。两者之间的差异mu1 - mu0,就是T学习器对该用户个体处理效应的估计值。

平均来看,这个个体差异的值约为+0.026,标准差为0.010。这种效应并不是均匀分布的:高参与度的用户显示出的个体差异约为+0.040,中等参与度的用户约为+0.013,低参与度的用户约为+0.030。正是这种因用户而异的差异,使得这种反事实估计方法比单一的平均提升值更加有用。

然而,当数据分布不平衡时,T学习器的效果就会大打折扣。如果每个处理组只有25,000条观测数据,那么使用这个模型还是可行的;但如果处理组只有200名用户,而对照组有4,800名用户(这种比例在某些小规模测试中很常见),m1模型就会严重缺乏训练数据,其预测结果也就不可信了。而下一步要介绍的X学习器,正是为了解决这类问题而设计的。

步骤2:用于处理组不平衡情况的X学习器

Künzel等人(2019年)提出的X-learner通过三阶段方法来处理数据不平衡的问题。第一阶段采用与T-learner相同的结局模型进行拟合;第二阶段计算出被估算的个体效应,并据此拟合第二阶段的tau模型;第三阶段则利用倾向得分作为权重,将这些估计值结合起来。

第二阶段a:被估算的效应

# 第二阶段a:被估算的效应
# 对于接受治疗的用户:观察结果减去控制组模型预测的结果
D1 = outcome[treatment == 1] - m0.predict_proba(X_arr[treatment == 1])[:, 1]

# 对于未接受治疗的用户:治疗组模型预测的结果减去观察结果
D0 = m1.predict_proba(X_arr[treatment == 0])[:, 1] - outcome[treatment == 0]

print(f"被估算的效应D1(治疗组):均值={D1.mean():.4f},标准差={D1.std():.4f}")
print(f"被估算的效应D0(对照组):均值={D0.mean():.4f},标准差={D0.std():.4f}")

预期输出:

被估算的效应D1(治疗组):均值=0.0280,标准差=0.1520
被估算的效应D0(对照组):均值=0.0240,标准差=0.1490

具体计算过程如下:D1表示每个接受治疗用户的残差值,即他们的实际结果与具有相同协变量特征的用户在方案A下的预期结果之间的差异。

D0对于未接受治疗的用户来说,则表示他们在方案B下的预期结果与实际结果之间的差异。

这两种被估算的效应都是基于整个数据集得出的、存在一定误差的治疗效果估计值。

第二阶段b:tau模型

from sklearn.linear_model import Ridge

# 第二阶段b:根据被估算的效应拟合tau模型
tau1_model = Ridge()
tau0_model = Ridge()

tau1_model.fit(X_arr[treatment == 1], D1)   # 将特征与治疗组的效应关联起来
tau0_model.fit(X_arr[treatment == 0], D0)   # 将特征与对照组的效应关联起来

tau1 = tau1_model.predict(X_arr)   # 治疗组效应的预测值
tau0 = tau0_model.predict(X_arr)   # 对照组效应的预测值

具体作用是:tau1_model通过岭回归分析,研究了接受治疗用户的个体治疗效果与协变量之间的关系;tau0_model则对未接受治疗用户进行了同样的分析。这两个模型都会为数据集中的每个用户生成预测值,从而得到两个独立的CATE估计值,最终会在第三阶段将这些估计值结合起来进行综合分析。

第三阶段:基于倾向得分的加权组合

# 第三阶段:结合倾向得分进行加权计算
ps_model = LogisticRegression(max_iter=1000)
ps_model.fit(X_arr, treatment)
e_x = ps_model.predict_proba(X_arr)[:, 1]   # P(T=1 | X)

# 加权组合:倾向得分较低的区域更依赖于tau1模型(治疗组模型)的预测结果
cate_x = e_x * tau0 + (1 - e_x) * tau1

print(f"\nX-learner模型的平均CATE值:{cate_x.mean():+.4f}")
print(f"X-learner模型的CATE标准差:{cate_x.std():.4f}")
print(f"倾向得分的范围:[{e_x.min():.4f}, {e_x.max():.4f}]")

预期输出:

X学习器的平均CATE值为:+0.0260
X学习器的CATE标准差为:0.0100
倾向值范围为:[0.4820, 0.5170]

这些估算结果反映了每个用户的实际表现与具有相同特征的典型用户在另一种提示条件下可能取得的成绩相比,究竟好出了多少或差了多少。随后,岭回归分析会进一步研究这些个体差异是如何随协变量变化的。

倾向得分用于确定权重分配方式:当倾向值较高时(即有大量类似用户接受了处理),X学习器会更信任tau0这个数值,因为这类观察数据的数量较多;而当倾向值较低时,系统会依赖对照组模型来进行分析,因为该组的数据密度更高。

在这个数据分布均衡的样本集上,X学习器的平均CATE值约为+0.026,这一结果与T学习器的结果几乎相同。这是预期之中的结果:在数据平衡的情况下,这两种估算方法应该会得出相似的结果。这种内部一致性证明了计算过程中不存在数值错误,但并不能证明我们真正得到了正确的结果。

当数据分布不平衡时,X学习器的优势才会显现出来:当倾向值偏向0.10这个区间时,其加权后的预测结果会明显优于T学习器。在数据均衡的样本集上这种差异是看不出来的,但还是建议进行这样的分析,因为接下来要处理的数据集很可能不会这么理想。

步骤3:自助法置信区间

如果没有不确定性范围,仅凭点估计值是无法做出明智决策的。自助法置信区间通过有放回地重新抽样数据,并对每次重采样后的数据重新应用整个估算流程来计算置信区间。

500次重抽样听起来可能很多,但实际上并不算过多。如果重抽样次数较少,置信区间的宽度确实难以稳定下来,此时所得到的结果很可能会受到随机误差的影响。如果希望得到符合学术发表标准的置信区间,建议进行1000次重抽样。

np.random.seed(7)
n = len(df)
n_boot = 500
boot_means_t = []
bootMeans_x = []

for i in range(n.boot):
    idx = np.random.choice(n, n, replace=True)
    Xb = X_arr[idx]
    tb = treatment[idx]
    yb = outcome[idx]

    # T学习器在自助样本上的表现
    mb0 = LogisticRegression(max_iter=500)
    mb1 = LogisticRegression(max_iter=500)
    mb0.fit(Xb[tb == 0], yb[tb == 0])
    mb1.fit(Xb[tb == 1], yb[tb == 1])

    mu0b = mb0.predict_proba(Xb)[:, 1]
    mu1b = mb1.predict_proba(Xb)[:, 1]
    boot_means_t.append((mu1b - mu0b).mean())

    # X学习器在自助样本上的表现
    D1b = yb[tb == 1] - mb0.predict_proba(Xb[tb == 1])[:, 1]
    D0b = mb1.predict_proba(Xb[tb == 0])[:, 1] - yb[tb == 0]

    t1b = Ridge(); t1b.fit(Xb[tb == 1], D1b)
    t0b = Ridge(); t0b.fit(Xb[tb == 0], D0b)

    tau1b = t1b.predict(Xb)
    tau0b = t0b.predict(Xb)

    psb = LogisticRegression(max_iter=500)
    psb.fit(Xb, tb)
    eb = psb.predict_proba(Xb)[:, 1]

    cate_xb = eb * tau0b + (1 - eb) * tau1b
    boot_means_x.append(cate_xb.mean())

bootMeans_t = np.array(boot_means_t)
bootMeans_x = np.array(boot_means_x)

ci_t = (nppercentile(boot_means_t, 2.5), nppercentile(boot_means_t, 97.5))
ci_x = (nppercentile(boot_means_x, 2.5), nppercentile(boot_means_x, 97.5))

print(f"T学习器的平均CATE值为:{bootMeans_t.mean():+.4f}")
print(f"T学习器95%置信区间为:[{ci_t[0]:+.4f}, {ci_t[1]:+.4f}]")
print()
print(f"X学习器的平均CATE值为:{boot_means_x.mean():+.4f}")
print(f"X学习器95%置信区间为:[{ci_x[0]:+.4f}, {ci_x[1]:+.4f}]")

预期输出:

T学习器的平均CATE值:+0.0260
T学习器95%置信区间:[+0.0120, +0.0400]

X学习器的平均CATE值:+0.0260
X学习器95%置信区间:[+0.0120, +0.0400]

具体操作过程如下:在500次迭代中,每次都会抽取与原始样本大小相同且允许重复抽样的子样本,然后从头开始重新构建所有模型(包括结果模型、插补效应模型以及倾向性模型),计算该子样本的平均CATE值,并将结果保存下来。

完成所有迭代后,会取保存下来的数值中第2.5百分位数和第97.5百分位数,作为95%置信区间的下限和上限。通过对两种学习器都进行这种自助法分析,可以确认这些不确定性估计值是一致的,这也是对结果可靠性的进一步验证。

当这两个置信区间都不为零时(正如当前的情况),就意味着有统计学上的证据表明提示B的效果优于提示A。如果置信区间包含了零,那就说明观察到的差异可能仅仅是由抽样误差造成的;在这种情况下,要么需要通过前瞻性实验来获得更明确的证据,要么就需要权衡错误决策所带来的风险,判断其是否可以接受。而像当前这种情况,由于置信区间完全为正数,因此就可以选择有针对性地推广这种提示方式,同时继续监测其中可能出现的异常情况。

这些置信区间的宽度相对于中心估计值来说还是相当大的——在中心估计值为2.6个百分点的情况下,两侧的置信区间宽度约为3.8个百分点。这样的宽度反映了真正的不确定性,这也是客观、诚实的评估结果。虽然进行超过500次自助法迭代可以减少这些置信区间的误差范围,但并不能改变潜在不确定性的真实程度。

步骤4:将CATE转换为策略价值

平均CATE值告诉了我们使用提示B所能带来的平均收益提升幅度。但对于产品决策而言,真正需要的是“策略价值”:如果将每个用户分配到被认为最能帮助他们的治疗方案中,那么与不采取任何措施相比,最终的总体收益提升幅度是多少呢?

制定策略的规则非常简单:对于那些预计受益程度超过你设定的阈值的用户,就使用提示B;而对于其他用户,则继续使用提示A。然后计算这种策略所带来的实际效果,与不采取任何措施相比有什么区别:

# 使用第一步中得到的T学习器的CATE值
threshold = 0.020   # 对于预计受益程度超过2个百分点的用户,使用提示B

policy_mask = cate_t > threshold
n_policy = policy_mask.sum()
meancate_policy = cate_t[policy_mask].mean()
total_lift = cate_t[policy_mask].sum()

print(f"策略阈值:           CATE > {threshold:.3f}")
print(f"使用提示B的用户数量:{n_policy} / {n} ({n_policy/n*100:.1f}%)")
print(f"使用提示B的用户的平均CATE值:{mean_cate_policy:+.4f}")
print(f>预计总收益提升幅度:{total_lift:.0f}次完成量")

# 比较对所有用户都使用提示B与有选择地使用提示B的效果
print(f"\n对所有用户使用提示B:{cate_t.mean():+.4f}平均CATE值")
print(f"有选择地使用提示B(针对受益程度超过{threshold}的用户):{mean_cate_policy:+.4f}每个用户的平均CATE值")
print(f>被选中使用提示B的用户占比:{n_policy/n*100:.1f}%")

# 基线情况:对所有用户都使用提示A,收益提升为0
# 策略价值 = E[CATE | CATE > threshold] * 被选中使用的用户比例
policy_value = meancate_policy * (n_policy / n)
print(f"\n策略价值(整个群体中每个用户的平均收益提升幅度):{policy_value:+.4f}")

预期结果:

政策阈值:CATE > 0.020
收到提示B的用户数量:35,000 / 50,000(70.0%)
该政策组中用户的平均CATE值为:+0.0320
预计带来的额外完成次数:1,120次

通过根据CATE的预估值来选择用户,而不是对所有用户统一应用该政策,你可以获得更高的平均效果,因为这样你能有针对性地排除那些接收提示B后效果不佳或几乎没有任何收益的用户。

在这个数据集中,当阈值设为0.020时,约有35,000名用户(占70%)会收到提示B,而这一组用户的平均CATE值为+0.032;如果对所有用户统一应用该政策,平均CATE值仅为+0.026。

关于阈值的选择,需要权衡以下几个方面:0.020这个阈值并不是绝对理想的。较高的阈值会减少被选中的用户数量,但因此每个被选中用户的平均效果会更显著;然而,这样就会错过那些本应从中受益的用户。较低的阈值虽然能覆盖更多用户,但却可能会将一些证据不足的用户也包括进来。

在任何实际应用中,你都应该同时呈现政策带来的效果以及第3步计算得出的95%置信区间。在这个例子中,置信区间大约为[+0.009, +0.047],这意味着如果阈值设置得过低,即使对部分用户有效,也可能导致整体效果不如统一应用该政策时好。因此,在设定阈值时,不仅要考虑点估计值,还要考虑这个置信区间。

当反事实估算失败时

元学习模型是通过某些假设来得出结果的。在使用这些反事实估算来做出决策之前,你必须识别这些假设可能存在的失效情况。

模型设定错误

T学习器和X学习器都会继承它们所基于的监督学习模型中存在的各种偏差。如果用户特征与任务完成结果之间的真实关系具有很强的非线性,而你使用的是逻辑回归模型(就像本教程中所做的那样),那么你的预测模型就会产生误差,从而导致CATE的估算值不准确。

在实际应用中,当你更换基础学习模型时,如果平均CATE的值发生了显著变化,就能说明这些估算值对模型形式的假设非常敏感,而这些假设可能并不成立。例如,如果从逻辑回归模型切换到梯度提升模型后,平均CATE的值从+0.026降到了+0.012,这种变化就表明你的估算结果容易受到不准确假设的影响。

为了解决这个问题,你可以使用更加灵活的基础学习模型(比如梯度提升模型或随机森林模型),并检查所选基础学习模型是否会对CATE的估算值产生显著影响。不同模型家族之间的稳定性是判断估算结果是否可靠的最佳依据。

正性偏差问题

要进行反事实估计,就必须确保样本中的每一位用户都有可能接受其中任意一种处理方式。如果那些参与度较高的用户有95%的概率被系统地引导去使用方案B,而参与度较低的用户只有5%的概率被引导使用该方案,那么倾向性模型就会正确地记录下这些极端数据;而对于那些被归入高/低参与度组别的用户来说,他们所对应的反事实估计结果实际上几乎没有任何真实数据作为支撑。

X-learner的加权组合几乎将所有权重都分配给了针对那些具有极端行为倾向的用户所建立的模型,而该模型是基于非常少量的可比观测数据训练得到的(这意味着你计算得出的CATE估计值会与你所观察到的路由偏差方向一致,都是错误的)。在解读处于边缘群体的用户的个体级CATE估计值之前,务必先检查他们的倾向得分分布情况。

未测量的混杂因素

这种问题最难防范,因为这些因素在数据中是看不见的。如果存在某些因素既决定了用户会收到什么样的提示信息,又影响了他们完成任务的效率,而这些因素并没有被包含在你的特征矩阵中,那么本教程中提到的所有CATE估计值都会将这一缺失的信息“吸收”进来,从而使得估计结果出现偏差。

我就见过这样的情况:当提示信息的选择部分受到工作空间大小的影响时,较大的工作空间既会带来更复杂的查询需求,也会提供更好的任务完成环境。如果你没有在X_cols中包含工作空间大小这一变量,你的估计结果就会将工作空间大小带来的影响与提示信息的效果混为一谈。

在这种情况下,只有通过严谨的特征工程设计和深入的行业知识才能有效防范这种问题。没有任何统计方法能够检测出那些你没有测量到的因素。

不重叠的协变量支持

如果处理组与对照组所处的协变量空间完全不同(即没有具有相似特征的共享用户),那么元学习器就只能从其中一组数据中推断出另一组数据的特征。这种推断完全依赖于你假设的功能形式(例如在岭回归示例中假设的是线性关系),而数据中并没有任何重叠区域可以作为这种推断的依据。

在实际应用中,当你发现某些大子群体的倾向得分都集中在0或1附近时,就应该警惕这种情况。在信任任何来自协变量分布不平衡的数据集的CATE估计结果之前,先运行倾向得分的重叠分析图、按协变量进行的分布比较,以及各组数据之间的标准化均值差异计算。

SUTVA原则的违反

反事实估计假设每个用户的最终结果仅取决于他们所接受的处理方式。但在协作式AI产品中(比如共享工作空间、团队总结功能、代码审核辅助工具等),某个用户的提示信息可能会出现在其他同事的界面中,因此某个用户所接受的处理方式也会直接影响其他团队的成员。

当SUTVA原则被违反时,个体级CATE估计值就会将直接的处理效果与用户所在网络带来的间接影响混为一谈。如果你的产品存在团队层面的交互作用,你就会发现个体级的估计结果往往偏高,并且在实际应用中也无法保持稳定。在这种情况下,应该使用群体级别的估计方法,而不是单独的元学习器。

策略性实施

上面介绍的实施方法都是为了清晰地展示各个步骤而已,在实际的生产环境中,还需要使用更复杂的模型。用梯度提升分类器(scikit-learn中的GradientBoostingClassifier)代替逻辑回归模型,可以捕捉到线性模型所忽略的非线性协变量交互作用。上面提到的T-learner和X-learner代码,其实可以与任何兼容sklearn的估计器一起使用,唯一需要改变的就是实例化的模型类而已。

对于需要自动模型选择、双重鲁棒估计方法以及内置诊断功能的生产级CATE估计算法,可以使用econmlcausalml这两个工具包。这两个工具包都通过统一的API实现了X-learner、T-learner、DR-learner以及因果森林算法,并且能够生成准确的置信区间。 本教程中提供的从头开始实现的版本构建速度较慢,代码阅读起来也较为繁琐。这正是其目的所在:在了解这些工具包的具体工作原理之前,你很难预测它们可能会在哪里出错。 通过使用“影子流量”来进行大规模的评估,效果会得到显著提升。在任何面向用户的更新发布之前,将一小部分生产环境中的查询请求路由到“Prompt B”系统中,就可以安全地记录下相应的实验结果。通过对这些“影子数据”进行反事实分析,你就可以获得关于真实生产环境的估计值,而无需承担任何实际部署的风险。 本教程配套的代码笔记本位于github.com/RudrenduPaul/product-experimentation-causal-inference-genai-llm/tree/main/10_counterfactual/prompts地址。你可以克隆这个代码仓库,生成合成数据集,然后运行counterfactual_demo.ipynb文件来逐个测试其中的代码块。 你的团队在“Prompt A”系统发布那周收集到的日志数据,恰恰包含了你解决那些深夜战略决策问题所需要的信息。你根本不需要去创建一个被遗忘的对照组;你需要的是一个能够准确预测用户在另一种情况下会采取何种行动的模型,并且这个模型的估计值需要具有较高的置信度;同时,还需要制定一条规则,只有当证据足够充分时,才允许系统向用户发送相应的提示。 构建这样一个模型,检查其中可能存在的故障模式,谨慎地设定阈值,然后利用这个模型来做出决策——这样肯定比凭直觉行事要好得多。

相关文章

技术实践

如何利用提示工程与上下文工程来开发人工智能代理

在这个教程中,我将向您展示提示工程和上下文工程如何提升人工智能模型的性能。 我们将构建一个简单的本地模型,从基础输入开始,然后通过使用更合适的提示语和更丰富的上下文信息来改进它,这样您就能看到每一项改变对最终输出结果的影响。 我们将会使用LangChain v1、Ollama、Qwen以及Python。所有操作都在您的个人电脑上完成,因此您无需支付任何API费用。 目录 背景知识 什么是提示工程? 什么是上下文工程? 为什么提示工程和上下文工程对人工智能模型如此重要 动机与架构 步骤1:安装Ollama并下载模型 步骤2:安装Python相关依赖库 步骤3:编写代理代码 示例输出结果 提示语优

阅读全文
技术实践

如何利用Gemini构建人工智能功能:面向开发者的提示工程实用指南

大多数关于提示工程的教学教程都遵循相同的流程:安装SDK,输入API密钥,调用 generateContent 函数,然后打印输出结果。模型会生成一些看似合理的内容,之后教学教程也就结束了。 但当你真正尝试将这个系统投入实际使用时,才会发现其实真正的准备工作根本还没有开始。 “API返回的文本”与“让用户感到可信的实际功能”之间的差距,正是需要耗费大量精力去解决的地方。 这个差距中充满了各种棘手的问题:模型生成的内容听起来和其他聊天机器人没什么两样;它会编造用户从未说过的话;它返回的数据会被用Markdown格式包裹起来;系统会在凌晨2点出现故障;而对于那些只是想得到答案的用户来说,系统展示的

阅读全文
技术实践

如何使用MONAI在超声数据上训练肿瘤分割模型

大多数分割教程都是从选择一个模型开始,将图像输入该模型中,然后调整超参数直到相关指标得到改善。但这种方法忽略了通常最为关键的一步:理解数据本身。 在本教程中,我们首先会对数据集进行详细分析,随后会根据这些分析结果来决定MONAI分割流程中的每一个设计细节。 我们将涵盖以下内容: 本教程适合谁? 关于数据集 什么是MONAI,为什么使用它? 什么是Dice评分? 第1部分——建模前的数据分析 类别平衡对分割结果的影响 患者数量对数据划分的影响 第2部分——构建分割流程 单一配置对象 按患者分组的数据划分方式 由快照自动选择的转换操作 模型、损失函数与评估指标 结果解读 预测结果可视化 失败模式比

阅读全文
技术实践

如何使用Node.js和Google Gemini通过函数调用来构建一个人工智能代理

github.com/ziaongit/nodejs-gemini-agent 。 目录 功能调用机制的工作原理 我们正在构建什么 先决条件 项目设置 工具的定义 工具功能的实现 构建智能代理的循环机制 命令行入口点的设置 添加Express HTTP服务器 智能代理的测试 故障排除 接下来要构建什么 功能调用机制的工作原理 这里有一个让人感到惊讶的地方:Gemini并不会直接运行你的代码。它只会返回一个结构化对象,其中包含诸如“调用 get_weather 函数、将 city 设置为柏林”这样的指令。你的代码会接收到这些指令,然后执行相应的功能并将结果反馈回去。Gemini会检查这些结果是否

阅读全文