☰
从抛硬币到A/B测试:手把手Python实现贝叶斯模型
2026/10/22 21:13:03 网站建设 项目流程

1. 从一个“猜硬币”问题说起:为什么我们需要贝叶斯模型

很多人第一次接触贝叶斯,是在概率论课本里看到那个长得像天书一样的公式:P(A|B) = P(B|A) * P(A) / P(B)。公式本身不难,难的是理解它到底在说什么,以及为什么它比我们直觉中的概率计算方式更“聪明”。

我举个特别接地气的例子。假设你手里有一枚硬币,但你不知道它是不是公平的——也就是说,它正面朝上的概率可能是0.5,也可能是0.3,甚至可能是0.9。现在你连续抛了10次,结果是7次正面、3次反面。请问,这枚硬币正面朝上的概率最可能是多少?

频率学派会说:这还用问?7除以10,0.7呗。但贝叶斯学派会说:等一下,在你抛硬币之前,你对这枚硬币有没有什么“先验认知”?比如,你从口袋里随便摸出一枚硬币,你大概率会认为它是公平的,正面概率在0.5附近。这个“先验认知”加上你抛出来的数据,才能得出一个更合理的“后验判断”。

这就是贝叶斯模型最核心的思想:先验知识 + 观测数据 = 后验判断。它不是简单地用数据说话,而是把我们对事物的已有认知也纳入计算。这个思路在实际工程中极其有用,因为很多时候我们拿到的数据量很少,光靠数据本身根本不足以得出可靠结论,必须借助先验知识来“兜底”。

这篇文章我会用一个完整的Python实例,手把手带你实现一个简单的贝叶斯模型。从问题定义、先验选择、似然函数构造,到后验计算和可视化,每一步我都会解释清楚“为什么这么做”以及“实际跑的时候要注意什么”。代码可以直接复制运行,适合有一定Python基础、但对贝叶斯方法还不太熟悉的朋友。如果你之前被贝叶斯公式劝退过,这次我争取让你彻底搞明白它到底在算什么。

2. 贝叶斯模型的核心思路拆解

2.1 先验、似然、后验:三个概念一次讲透

贝叶斯模型说到底就是在做一件事:根据新证据更新信念。这里面涉及三个关键概念,我用一个生活场景来解释。

假设你新认识了一个朋友,你觉得他大概率是个程序员(先验判断),因为他的穿着打扮、说话方式让你有这种感觉。后来你发现他每天中午都在看技术博客,这个新证据让你更加确信他是程序员(后验更新)。但如果有一天你发现他居然在朋友圈晒了一张烹饪大赛的奖状,你可能就要重新评估了——也许他是厨师,只是对技术感兴趣。

对应到数学上:

  • 先验概率 P(θ):在看到任何数据之前,你对参数θ的信念。比如你认为硬币正面概率最可能是0.5。
  • 似然函数 P(D|θ):在给定参数θ的情况下,观测到当前数据D的概率。比如如果硬币正面概率真是0.7,那么抛出7正3反的概率是多少。
  • 后验概率 P(θ|D):看到数据D之后,你对参数θ的更新信念。这是我们最终想要的结果。

贝叶斯公式把这三者串起来:P(θ|D) = P(D|θ) * P(θ) / P(D)。其中P(D)是边际似然,起到归一化的作用,保证后验概率加起来等于1。

注意:很多人卡在“为什么要有先验”这个问题上。其实先验不是“拍脑袋”,而是对问题背景的合理编码。如果你真的没有任何先验信息,可以用均匀分布作为无信息先验,让数据自己说话。

2.2 为什么选共轭先验:让计算从地狱变天堂

理论上,贝叶斯公式对任何先验和似然都成立。但实际计算时,分母P(D)往往是一个积分,在高维参数空间里根本算不出来。这时候共轭先验就派上用场了。

所谓共轭先验,就是先验分布和后验分布属于同一分布族。比如:

似然函数共轭先验后验分布
伯努利分布Beta分布Beta分布
正态分布(已知方差)正态分布正态分布
泊松分布Gamma分布Gamma分布

这意味着你不需要做复杂的积分运算,只需要更新分布的几个参数就行了。拿Beta-伯努利模型来说,先验是Beta(α, β),观测到k次成功、n-k次失败后,后验直接就是Beta(α+k, β+n-k)。简单到令人发指。

我选择用Beta-伯努利模型来做这个例子,原因有三:第一,它是最简单的共轭模型,数学门槛低;第二,硬币抛掷问题直观易懂,不需要领域知识;第三,它的后验更新过程可以可视化,能让你亲眼看到“信念如何随数据更新”。

2.3 完整建模流程:从问题到代码的映射

在动手写代码之前,先把整个建模流程理清楚。我习惯把贝叶斯建模分成五步:

  1. 定义参数和参数空间:明确我们要估计什么。这里就是硬币正面朝上的概率θ,取值范围[0, 1]。
  2. 选择先验分布:根据已有认知选择Beta(α, β)。α和β的选择有讲究,后面会详细讲。
  3. 构造似然函数:根据数据生成机制写出P(D|θ)。抛硬币服从伯努利分布,n次独立抛掷的似然就是θ^k * (1-θ)^(n-k)。
  4. 计算后验分布:利用共轭性质直接得到Beta(α+k, β+n-k)。
  5. 后验分析与决策:计算后验均值、可信区间,或者做预测。

这五步不仅适用于这个简单例子,换成任何贝叶斯模型都是类似的套路。区别只在于先验和似然的形式不同,以及后验是否能用解析形式表达。如果不能用解析形式,就需要用到MCMC采样方法,那是另一个话题了。

3. 手把手Python实现:从零搭建贝叶斯更新模型

3.1 环境准备与依赖安装

这个例子用到的库很少,都是数据科学的基础工具:

pip install numpy scipy matplotlib
  • numpy:做数值计算,生成随机数。
  • scipy:提供Beta分布的概率密度函数和统计量计算。
  • matplotlib:画图,可视化先验和后验分布。

我用的Python版本是3.9,但3.7以上应该都没问题。如果你用Anaconda,这些库都是自带的,直接跳过安装步骤。

提示:不建议用pip install scipy这种方式在全局环境安装,最好建一个虚拟环境。我吃过亏,不同项目对scipy版本要求不一样,全局安装容易冲突。

3.2 定义先验分布:Beta分布参数怎么选

先验分布的选择是贝叶斯建模中最需要动脑子的地方。对于硬币问题,我们选Beta(α, β)作为先验。Beta分布的定义域是[0, 1],正好对应概率的取值范围,而且它的形状非常灵活。

α和β的物理含义可以这样理解:α-1相当于“先验中正面出现的次数”,β-1相当于“先验中反面出现的次数”。比如Beta(1, 1)就是均匀分布,表示没有任何先验偏好;Beta(5, 5)表示你比较确信硬币是公平的;Beta(2, 8)表示你倾向于认为硬币偏向反面。

import numpy as np from scipy import stats import matplotlib.pyplot as plt # 定义先验参数 alpha_prior = 2 beta_prior = 2 # 生成theta的取值范围 theta = np.linspace(0, 1, 1000) # 计算先验概率密度 prior = stats.beta.pdf(theta, alpha_prior, beta_prior) # 画先验分布 plt.figure(figsize=(10, 6)) plt.plot(theta, prior, 'b-', label=f'Prior Beta({alpha_prior}, {beta_prior})') plt.xlabel('θ (probability of heads)') plt.ylabel('Density') plt.title('Prior Distribution') plt.legend() plt.grid(True, alpha=0.3) plt.show()

这段代码跑出来是一个钟形曲线,峰值在0.5附近。选Beta(2, 2)的意思是:我轻微倾向于硬币是公平的,但不确定,允许它有偏差。如果你完全没概念,用Beta(1, 1)也行,那就是一条水平线。

注意:先验参数不要设得太大。比如Beta(100, 100)意味着你极其确信硬币是公平的,这时候就算抛100次得到70次正面,后验也几乎不动。先验太强会导致模型“固执己见”,对数据不敏感。

3.3 构造似然函数与模拟抛掷数据

似然函数的构造取决于数据生成机制。抛硬币是典型的伯努利试验,n次独立抛掷中正面出现k次的概率服从二项分布:

P(k|θ) = C(n, k) * θ^k * (1-θ)^(n-k)

其中C(n, k)是组合数,在贝叶斯更新中它作为常数会被归一化掉,所以实际计算时只需要关注θ^k * (1-θ)^(n-k)这部分。

现在模拟一组数据。假设真实硬币正面概率是0.7,我们抛20次:

# 设定真实参数(实际中我们不知道这个值) true_theta = 0.7 n_tosses = 20 # 模拟抛掷 np.random.seed(42) tosses = np.random.binomial(1, true_theta, n_tosses) n_heads = np.sum(tosses) n_tails = n_tosses - n_heads print(f"抛掷结果: {tosses}") print(f"正面次数: {n_heads}, 反面次数: {n_tails}")

跑出来结果大概是正面14次、反面6次。注意这里设了随机种子,保证你每次跑出来的结果一样,方便对照。

似然函数的代码实现:

# 计算似然函数 likelihood = theta**n_heads * (1 - theta)**n_tails # 归一化以便画图(似然函数本身不是概率密度,积分不为1) likelihood_normalized = likelihood / np.trapz(likelihood, theta) plt.figure(figsize=(10, 6)) plt.plot(theta, likelihood_normalized, 'g-', label='Likelihood') plt.xlabel('θ') plt.ylabel('Normalized Likelihood') plt.title('Likelihood Function') plt.legend() plt.grid(True, alpha=0.3) plt.show()

似然函数的峰值在14/20=0.7附近,这正是频率学派的估计值。但贝叶斯不会只报告这个点估计,而是给出整个后验分布。

3.4 计算后验分布:共轭更新的代码实现

利用Beta-伯努利共轭性质,后验分布直接是Beta(α + k, β + n - k):

# 后验参数更新 alpha_posterior = alpha_prior + n_heads beta_posterior = beta_prior + n_tails print(f"先验: Beta({alpha_prior}, {beta_prior})") print(f"后验: Beta({alpha_posterior}, {beta_posterior})") # 计算后验概率密度 posterior = stats.beta.pdf(theta, alpha_posterior, beta_posterior) # 把先验、似然、后验画在一起 plt.figure(figsize=(12, 7)) plt.plot(theta, prior, 'b--', label=f'Prior Beta({alpha_prior}, {beta_prior})', alpha=0.7) plt.plot(theta, likelihood_normalized, 'g--', label='Likelihood', alpha=0.7) plt.plot(theta, posterior, 'r-', label=f'Posterior Beta({alpha_posterior}, {beta_posterior})', linewidth=2) plt.axvline(true_theta, color='k', linestyle=':', label=f'True θ = {true_theta}') plt.xlabel('θ') plt.ylabel('Density') plt.title('Bayesian Update: Prior + Likelihood = Posterior') plt.legend() plt.grid(True, alpha=0.3) plt.show()

这张图是整个博文的核心。你会看到:先验(蓝色虚线)比较宽,说明不确定性大;似然(绿色虚线)比较窄,集中在0.7附近;后验(红色实线)是先验和似然的“折中”,峰值在0.65左右,比似然的峰值稍微偏向先验的0.5。

这就是贝叶斯更新的直观体现:数据把我们的信念往真实值方向拉,但先验又把它往回拽了一点。如果数据量更大,比如抛200次,似然会变得非常窄,后验就几乎完全由数据决定了。

3.5 后验分析与可视化:可信区间与点估计

后验分布拿到之后,可以做很多有意思的分析。最常用的是计算后验均值和95%可信区间:

# 后验均值 posterior_mean = alpha_posterior / (alpha_posterior + beta_posterior) # 95%可信区间 credible_interval = stats.beta.interval(0.95, alpha_posterior, beta_posterior) print(f"后验均值: {posterior_mean:.4f}") print(f"95%可信区间: [{credible_interval[0]:.4f}, {credible_interval[1]:.4f}]")

后验均值大概是0.667,95%可信区间大概是[0.45, 0.85]。注意,贝叶斯的可信区间和频率学派的置信区间解释完全不同。可信区间的意思是“参数有95%的概率落在这个区间内”,而置信区间的意思是“如果重复采样无数次,95%的区间会包含真实参数”。前者更符合直觉,后者经常被误解。

还可以做后验预测:如果再用这枚硬币抛一次,正面朝上的概率是多少?

# 后验预测概率 # 对于Beta-伯努利模型,后验预测概率就是后验均值 pred_prob = posterior_mean print(f"下一次抛掷正面朝上的预测概率: {pred_prob:.4f}")

这个预测概率不是简单的14/20=0.7,而是0.667,因为它把先验信息也考虑进去了。随着数据量增加,预测概率会逐渐逼近真实值。

4. 实操中容易踩的坑与排查技巧

4.1 先验选择不当导致的“固执模型”

我刚开始用贝叶斯的时候,犯过一个典型错误:为了让模型“更准确”,把先验设得特别强。比如用Beta(50, 50)作为先验,结果抛了20次数据后,后验几乎没变化,还是集中在0.5附近。当时我还以为是代码写错了,排查了半天才发现是先验太强导致的。

先验的强度用等效样本量来衡量,α+β就是先验的“等效样本数”。Beta(50, 50)相当于你事先已经抛了100次硬币,而且结果正好是50正50反。这时候再加20次数据,就像往大海里倒一杯水,根本改变不了什么。

提示:如果你不确定先验该设多强,从Beta(1, 1)或Beta(2, 2)开始。这两个先验的等效样本量很小,数据能快速“覆盖”先验的影响。

4.2 数值计算中的下溢问题

当数据量很大时,似然函数θ^k * (1-θ)^(n-k)会变得非常小,可能导致数值下溢。比如抛1000次硬币,似然值可能小到10的负几百次方,计算机浮点数根本表示不了。

解决办法是在对数空间计算:

# 对数似然,避免下溢 log_likelihood = n_heads * np.log(theta + 1e-10) + n_tails * np.log(1 - theta + 1e-10) # 减去最大值再取指数,这是log-sum-exp技巧 log_likelihood -= np.max(log_likelihood) likelihood_stable = np.exp(log_likelihood)

这里加1e-10是为了防止log(0)的情况。虽然在这个简单例子里不会出问题,但养成好习惯很重要。实际项目中,MCMC采样、变分推断等方法都会在对数空间操作。

4.3 共轭先验不适用时怎么办

Beta-伯努利模型之所以简单,是因为共轭性质让后验有解析解。但现实问题往往没这么幸运。比如你想估计正态分布的均值和方差,两个参数都不知道,共轭先验就不存在了(除非用Normal-Inverse-Gamma,但那是另一个层面的复杂度)。

这时候就需要数值方法,最常用的是MCMC(马尔可夫链蒙特卡洛)。Python里可以用PyMC或Stan来做。但MCMC的计算成本比共轭更新高几个数量级,而且需要检查收敛性。我的建议是:能用共轭先验就用共轭先验,实在不行再上MCMC。

问题类型推荐方法计算成本
单参数,有共轭先验解析更新极低
多参数,无共轭先验MCMC高
高维参数变分推断中等

4.4 常见问题速查表

问题现象可能原因解决方法
后验几乎等于先验先验太强或数据太少减弱先验或增加数据
后验几乎等于似然先验太弱增强先验或接受数据主导
似然函数出现NaNlog(0)或数值下溢在对数空间计算,加小常数
可信区间过宽数据量不足增加样本量
后验多峰先验与数据冲突检查先验合理性或数据质量

5. 贝叶斯模型的扩展方向与实战价值

5.1 从硬币到A/B测试:贝叶斯在实际业务中的应用

硬币抛掷虽然简单,但它的模型结构可以直接迁移到很多实际场景。最常见的就是A/B测试。

假设你在做一个网页改版实验,A版本有1000个用户访问,转化了120个;B版本有1000个用户访问,转化了150个。频率学派会用卡方检验算p值,告诉你“差异是否显著”。但贝叶斯方法会直接告诉你:B版本转化率高于A版本的概率是97%,而且B版本转化率比A版本高多少的可信区间是[0.5%, 5.2%]。

这种输出对业务决策更有用,因为决策者关心的是“B比A好的概率有多大”,而不是“p值是否小于0.05”。实现上只需要把Beta-伯努利模型套进去就行,A版本的转化率后验是Beta(1+120, 1+880),B版本是Beta(1+150, 1+850),然后比较两个后验分布。

5.2 层次贝叶斯:让模型学会“举一反三”

简单贝叶斯模型假设所有数据来自同一个分布。但现实中数据往往有分组结构。比如你同时在10个渠道做A/B测试,每个渠道的转化率不同,但又有相似性。这时候可以用层次贝叶斯模型:每个渠道有自己的转化率θ_i,但这些θ_i又服从一个共同的先验分布。

这样做的好处是“部分池化”:数据多的渠道主要靠自己数据估计,数据少的渠道会借用其他渠道的信息。实际效果比单独估计每个渠道更稳定,尤其适合小样本场景。

5.3 贝叶斯思维对日常决策的启发

抛开数学和代码,贝叶斯模型最大的价值是提供了一种动态更新信念的思维方式。

很多人做判断时容易走极端:要么完全凭直觉(先验太强),要么完全被最新消息带偏(先验太弱)。贝叶斯思维告诉你,合理的做法是:有一个初始判断,然后根据新证据逐步调整,调整幅度取决于证据的可靠性和先验的确定性。

我在实际工作中经常用这个思路。比如评估一个技术方案是否可行,我会先根据经验给一个初始信心值,然后每做一次原型测试就更新一次信心。如果测试结果和预期差距很大,我会反思是数据有问题还是先验有问题。这种思维方式比“拍脑袋决定”或者“等所有数据都齐了再说”要高效得多。

代码仓库里我留了一个完整的Jupyter Notebook,包含所有代码和注释。你可以把n_tosses改成不同的值,观察后验分布的变化。我建议你至少试三组:n=5、n=50、n=500,亲眼看看数据量如何“碾压”先验。这个直观感受比看十篇文章都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询