做数据分析这行时间久了,会发现一个挺有意思的现象:很多人能背出联合概率、条件概率、贝叶斯法则这三个词的定义,甚至公式写得一字不差,但真到了业务场景里,比如一份体检报告显示某项指标阳性、一次线上事故需要定位是哪个模块出的问题、一封邮件要判断是不是垃圾邮件,立刻就卡住了。不是不会算,而是不知道该用哪一个,或者说不知道该把哪个量当成已知、哪个量当成要求。这三个公式真正的价值不在于考试,而在于它们构成了"用信息修正判断"这条完整链路:联合概率负责描述两件事一起发生的可能性,条件概率负责在拿到新信息之后重新分配可能性,贝叶斯法则负责从观察到的结果反推背后的原因。本文面向的是刚接触概率建模的开发者、需要做数据决策的产品和运营同学,以及那些"公式看得懂但用不出来"的从业者,我会把三个公式拆开讲透,每个都配上可以直接跑的数字和代码,重点放在它们在实际工作里到底解决了什么问题。
1. 先把三个公式串成一条线
1.1 一张体检单引出的三个问题
假设某次体检,某项肿瘤标志物检测结果是阳性。这个时候你脑子里会同时冒出三个层次的问题,而且这三个问题的难度是递进的。
第一个问题是:这项指标阳性,同时真的患病,这两件事一起发生的概率有多大?这是联合概率,问的是两个事件同时成立的可能性,写成 P(阳性 且 患病)。
第二个问题是:如果已经患病了,检测出阳性的概率有多大?这是条件概率,问的是在"患病"这个前提下"阳性"出现的可能性,写成 P(阳性|患病)。注意这两个问题的方向完全相反,第一个是"两个都发生",第二个是"一个发生的前提下另一个发生"。
第三个问题才是最要命的:现在我已经知道检测是阳性了,那我真正患病的概率有多大?这是 P(患病|阳性),正好是第二个问题的反向。它需要用到贝叶斯法则,因为我们要从"结果"倒推"原因"。
我见过太多人把第二问和第三问当成一回事。检测灵敏度 99%,就以为阳性之后患病概率也是 99%,这个错误在现实里造成的恐慌和过度治疗难以估量。后面第 4 章会用具体数字把这个差距算出来,结论是这两个数可能差一个数量级。
1.2 三个公式其实是一条链子
把三个公式写成一条链路,会清楚很多。联合概率是地基,它描述的是整个样本空间里各个格子的大小;条件概率是在地基上切一刀,把样本空间缩小到某个子集里再重新看比例;贝叶斯法则是把这一刀从两个方向切通,用一边的比例去反推另一边的比例。
可以用一个很生活的类比。假设有一个 1000 人的小区,我们按"是否养猫"和"是否养狗"把人分成四类,这就是联合分布,四个格子填满 1000 人。现在我问:在养猫的人里,养狗的比例是多少?这就把样本空间从 1000 人缩小到了养猫的那部分人,然后在这个子集里算比例,这就是条件概率。再反过来问:在养狗的人里,养猫的比例是多少?这又是另一个条件概率,而且它跟上一个通常不相等。
三个公式的关系可以整理成下面这样:
| 公式 | 表达式 | 回答的问题 | 典型误用 |
|---|---|---|---|
| 联合概率 | P(A∩B) = P(A)·P(B|A) | 两件事同时发生 | 默认 P(A∩B)=P(A)·P(B)(未验证独立) |
| 条件概率 | P(B|A) = P(A∩B)/P(A) | 已知 A 发生后 B 的概率 | 与 P(A|B) 混为一谈 |
| 贝叶斯法则 | P(A|B) = P(B|A)·P(A)/P(B) | 由结果反推原因 | 忽略先验 P(A),只看似然 |
这张表我建议直接记下来,尤其是第三列,它比公式本身更有用。公式可以现查,但"我现在手上有什么、我要算什么"这个判断必须在脑子里瞬间完成。
1.3 为什么"方向性"是这三个公式的核心
数学上 P(A|B) 和 P(B|A) 的区别一眼可见,但工程实践里这两个量经常被混淆,因为它们在人脑里的直觉描述非常接近。比如"吸烟者得肺癌的概率"和"肺癌患者中吸烟的比例",口语里都能说成"吸烟和肺癌的关系",但数值可以差很远。
造成这种混淆的根源是基准率被忽略了。假设某地吸烟率 20%,非吸烟率 80%;吸烟者患病率 5%,非吸烟者患病率 0.5%。那么 P(患病|吸烟)=5%,而 P(吸烟|患病) 需要算:患病的总概率是 0.2×0.05 + 0.8×0.005 = 0.01 + 0.004 = 0.014,其中吸烟贡献 0.01,所以 P(吸烟|患病) ≈ 71.4%。5% 和 71.4%,差了十几倍,但两者描述的是同一批数据。
提示:只要看到"某某人群中某特征比例很高"这类说法,先问一句基准率是多少。没有基准率的比例,信息量接近于零。
理解了这条链子,后面三章就可以一个一个拆。我的建议是,先别急着记贝叶斯公式,把联合概率和条件概率的直觉练扎实,贝叶斯法则其实就是条件概率定义式的一个移项,没什么新东西。
2. 联合概率:把"同时发生"这件事算明白
2.1 从样本空间切格子说起
联合概率 P(A∩B) 描述的是事件 A 和事件 B 同时发生的概率。最朴素的理解方式就是画格子:把所有可能的结果铺成一个区域,A 占一块,B 占一块,两块重叠的部分占比就是联合概率。
举个具体的。一批产品 100 件,其中有 3 件次品,另外还有 10 件来自某条特定产线(假设这两类属性有交叉)。我们从中随机抽一件,抽到"既来自该产线又是次品"的概率就是联合概率,它需要知道交叉部分到底有几件,而不只是知道两个边缘概率。
这就是很多人第一处栽跟头的地方:知道 P(A)=0.03、P(B)=0.10,就以为 P(A∩B)=0.003。这个乘法只在 A、B 相互独立的时候成立。如果不独立,比如那条产线的次品率明显偏高,交叉部分就会远大于这个数。
联合概率的通用计算式只有一条,就是乘法公式:
P(A∩B) = P(A) · P(B|A)
它不要求独立,什么时候都能用。代价是你必须先知道条件概率 P(B|A)。而当 A、B 独立时,P(B|A)=P(B),式子退化成 P(A)·P(B),这才是那个"直接相乘"的版本。所以"相乘"不是定义,是特例。
2.2 独立与不独立:为什么必须先判断
独立性的判断在工程上不是学术问题,它直接决定了你算出来的数能不能用。我遇到过不少分析报告,把两个明显相关的指标当成独立事件相乘,最后的概率估出来比实际小了一个数量级,导致风控阈值设得过于保守,误杀了一大批正常用户。
判断独立有两种路数。理论路数是看这两个事件在机制上是否互相影响,比如"用户是否点击广告"和"用户是否完成支付",这两件事显然共享同一个用户意图,不可能独立。数据路数是拿历史数据算一下 P(A∩B) 和 P(A)·P(B),看看差多少。如果差在 5% 以内,工程上可以近似当独立处理;如果差得多,就必须老老实实用条件概率。
有一个特别容易忽略的场景:同源特征。比如用"是否登录"和"是否有会员身份"两个特征做流失预测,这两个特征高度相关,因为会员通常都是登录状态。在朴素贝叶斯里如果把它们当独立处理,就会对同一份信息重复计数,导致概率估计过度自信。这个坑在第 5 章还会再展开。
2.3 有放回和无放回:差的不只是一个分母
联合概率里最经典的一组对比,就是从有限总体里连续抽两个。还是那批 100 件产品、3 件次品。
有放回的情况,抽完一件放回去再抽:两次都是次品的概率是 (3/100) × (3/100) = 0.0009。这里是独立的,因为第一次的结果不影响第二次的总体。
不放回的情况,抽完一件不放回:第一次抽到次品的概率是 3/100,在第一次已经抽到次品的前提下,剩下 99 件里还有 2 件次品,所以第二次也是次品的条件概率是 2/99,联合概率是 (3/100) × (2/99) = 6/9900 ≈ 0.000606。
两者差了大约 1.5 倍。差的这一点,就是"总体被消耗"带来的信息。样本量越小、事件越稀有,这个差距越明显。如果是 100 万里抽 3 个次品,那有放回和不放回几乎没区别。
这条结论在实操里非常有价值:做抽奖、做质检抽样、做 AB 实验分流,只要是从有限池子里不放回地取,就不能用独立相乘。很多抽奖活动的概率公示算错,就是错在这里。
2.4 工程里联合概率到底用在哪
不用把联合概率想得太玄,它的应用场景非常具体。
漏斗分析是把每一步的转化率连乘,这本质上是联合概率的链式展开:完成注册、完成绑卡、完成首单三步都发生,概率等于 P(注册) × P(绑卡|注册) × P(首单|注册且绑卡)。注意这里每一步用的都是条件概率,不是各自独立的转化率,因为后一步的转化率依赖于前面的筛选。
多维交叉筛选是另一个典型。运营想看"一线城市且 25 岁以下且近 30 天活跃"的用户规模,这个人群占比就是三个条件的联合概率,必须用链式乘法,而且顺序会影响计算精度和数据可得性。
风险评估里的共现事件更直接。一次交易同时出现"异地登录"和"大额转账",这两个事件如果各自都是低频事件,独立假设下联合概率会小到几乎为零,但实际共现概率可能是它的几百倍。所以风控规则里从来不直接乘,而是用历史共现数据拟合出来的联合分布。
2.5 用代码把联合概率跑一遍
光看公式容易虚,直接跑一段蒙特卡洛模拟,能一眼看出独立假设和不放回抽样的差距。
import numpy as np rng = np.random.default_rng(42) N = 200_000 def draw_without_replacement(n_iter): count = 0 for _ in range(n_iter): sample = rng.choice(100, size=2, replace=False) if sample[0] < 3 and sample[1] < 3: # 前3号为次品 count += 1 return count / n_iter def draw_with_replacement(n_iter): count = 0 for _ in range(n_iter): sample = rng.integers(0, 100, size=2) if sample[0] < 3 and sample[1] < 3: count += 1 return count / n_iter print("不放回理论值:", (3/100) * (2/99)) print("不放回模拟值:", draw_without_replacement(N)) print("有放回理论值:", (3/100) ** 2) print("有放回模拟值:", draw_with_replacement(N))跑出来的结果会非常接近理论值,误差在千分之几的量级。这段代码的意义不只是验证,更重要的是它是一个模板:任何你不确定联合概率怎么算的场景,都可以用模拟的方式先跑一个经验值,再跟你的解析解对照。对不上就一定是你哪里假设错了,这个排查手段比反复推公式快得多。
注意:
rng.choice(100, size=2, replace=False)里前 3 号代表次品这个约定,只是为了写起来方便,实际业务里要用真实的属性数组去筛选,别把编号当属性。
3. 条件概率:新信息进来后,概率怎么重算
3.1 条件概率的本质是缩小样本空间
条件概率的定义式很朴素:P(B|A) = P(A∩B) / P(A),要求 P(A) > 0。但它的直觉比公式重要得多——条件概率做的事情,是把整个样本空间砍掉大部分,只留下 A 发生的那部分,然后在这部分里重新衡量 B 的比例。
用小区那个例子。整个小区 1000 人,养猫的 200 人,其中同时养狗的有 60 人。那么 P(养狗|养猫) = 60/200 = 0.3。这里的除法,分母从 1000 变成了 200,这就是"缩小样本空间"。所有的条件概率计算,本质上都是在换分母。
这个理解一旦建立起来,很多问题会变得很清楚。比如为什么 P(A|B) 和 P(B|A) 不一样?因为一个是以 B 为分母,一个是以 A 为分母,分母不同,结果自然不同。再比如为什么条件概率可以链式相乘?因为每一步都是在上一步缩小后的空间里继续砍,砍法自然可以连起来。
3.2 乘法公式和全概率公式:两个必会的工具
乘法公式是联合概率那章的式子的移项:P(A∩B) = P(A)·P(B|A)。它把"同时发生"拆成"先发生 A,再在 A 的前提下发生 B"两段,这在计算复杂复合事件时非常有用,因为拆完之后每一段都好算。
全概率公式则是把样本空间按某个维度切成互斥的几块,然后加权求和:如果 B1、B2 构成一个完备划分(互斥且覆盖全部情况),那么 P(A) = P(A|B1)·P(B1) + P(A|B2)·P(B2)。
全概率公式的价值在于,它让你可以用"分情况讨论"的方式算一个总的概率。临床上按"患病/未患病"分情况算检测阳性的总概率,制造业里按"哪条产线生产"分情况算次品总概率,风控里按"正常用户/欺诈用户"分情况算触发规则的总概率,都是这个套路。
提示:全概率公式的分组必须是完备且互斥的,也就是"不重不漏"。漏掉一类是最常见的错误,会让分母偏小,最后算出来的后验概率系统性偏大。
3.3 完整案例:两个供应商的次品归因
这个案例我在实际工作里用过很多次,因为它同时用到了全概率公式和贝叶斯法则,而且结论很反直觉。
某公司某个零件由两家供应商供货,A 供应商供 60%,B 供应商供 40%。A 的次品率是 2%,B 的次品率是 5%。现在质检抽到一件次品,问它来自 B 供应商的概率是多少?
先分情况算总次品率,用全概率公式:
P(次品) = 0.6 × 0.02 + 0.4 × 0.05 = 0.012 + 0.020 = 0.032
也就是整体次品率 3.2%。注意这个数字比 A 的 2% 高、比 B 的 5% 低,符合加权平均的直觉。
再看 B 的贡献占比:
P(B|次品) = 0.020 / 0.032 = 0.625
结论是:抽到的这件次品,有 62.5% 的概率来自 B 供应商。而 B 只供货 40%,却贡献了 62.5% 的次品。这就是"次品归因"的标准算法。
这个结论反直觉的地方在于,很多人第一反应是"B 的次品率是 A 的 2.5 倍,所以次品里 B 占大头"。方向对了,但对具体数字没概念。而如果反过来只算 P(次品|A)=2% 和 P(次品|B)=5%,你会得出"整体次品率大概 5%"的错误判断,因为忽略了供货比例这个权重。
这个思路可以直接迁移到线上事故定位:几个模块的报错率不同、流量占比不同,要定位哪一个是主要来源,就是同一套计算。
3.4 条件概率最容易踩的三个坑
第一个坑是方向混淆,前面已经说过,这里再强调一次。写代码的时候变量名一定要带上方向,比如p_positive_given_disease和p_disease_given_positive两个变量名分开写,千万别都叫p。我见过一个项目里两个量共用同一个变量名,结果在某个分支里被覆盖了,模型上线后预测值系统性偏高,排查了两周才发现。
第二个坑是条件不是随机事件。条件概率要求条件本身是一个有概率的事件。如果条件是一个确定性的事实,比如"已知今天是周一",那 P(A|周一) 在数学上仍然可以算,但语义上要谨慎,因为它退化成"在周一这个子总体里的分布",而不是"周一这件事发生的概率"。工程上更需要注意的是"条件"和"分组"的区别。
第三个坑是用了不该用的历史数据。条件概率必须是同一时期、同一总体的数据。用去年的次品率去算今年的归因,如果工艺改过,结论就完全错了。这个坑在数据口径变化频繁的业务里特别常见。
4. 贝叶斯法则:从结果反推原因
4.1 一行推导,没什么神秘
贝叶斯法则的来源简单到有点乏味。联合概率的对称性告诉我们:
P(A∩B) = P(A)·P(B|A) = P(B)·P(A|B)
两边同时除以 P(B),就得到:
P(A|B) = P(A) · P(B|A) / P(B)
完事了。它没有引入任何新概念,就是同一个联合概率的两种写法互相除了一下。很多人觉得贝叶斯玄,是因为教材喜欢用哲学口吻讲"先验后验",把简单的东西讲复杂了。
真正需要理解的是这个式子里每个部分扮演什么角色。P(A) 是你在看到证据 B 之前对 A 的信念,叫先验;P(B|A) 是"如果 A 成立,证据 B 出现的可能性",叫似然;P(B) 是证据本身出现的总概率,叫证据或者归一化常数;P(A|B) 是看到证据之后更新过的信念,叫后验。
一句话概括:后验正比于先验乘似然。这句话比公式好用,因为在实际建模中,分母 P(B) 常常可以用全概率公式展开,或者在做比较时直接忽略(因为对所有候选 A 都一样)。
4.2 四个名词翻译成人话
先验 P(A):在没有任何新证据时,你认为这件事发生的概率。它可以从历史统计里来,可以从专家判断来,也可以在完全没信息时用均匀分布。先验的选择是贝叶斯方法里最受争议的部分,第 5 章会专门讲怎么定。
似然 P(B|A):假设 A 成立,观察到 B 的可能性。这个东西通常是模型给出的,比如检测设备的灵敏度、模型在正样本上的召回率,它是相对客观、可以从数据里估的。
证据 P(B):不管 A 成不成立,B 出现的总概率。它的作用是把后验归一化到 0 到 1 之间,所以很多时候不关心具体值,只关心相对大小。
后验 P(A|B):我们真正想要的答案。而且它可以作为下一轮的先验,继续往后推,这就是贝叶斯更新。
4.3 把那个体检案例算到底
回到开头那个问题。设某地区某病的患病率是 0.1%,检测的灵敏度(患病者检出阳性的概率)是 99%,特异度(未患病者检出阴性的概率)是 99%,也就是假阳性率 1%。现在某人检测阳性,他真正患病的概率是多少?
先算证据,也就是所有人里检测阳性的总比例,用全概率公式:
P(阳性) = P(阳性|患病)·P(患病) + P(阳性|未患病)·P(未患病) = 0.99 × 0.001 + 0.01 × 0.999 = 0.00099 + 0.00999 = 0.01098
然后套贝叶斯法则:
P(患病|阳性) = 0.00099 / 0.01098 ≈ 0.0902
答案是大约 9%。检测灵敏度 99%,但阳性之后真正患病的概率只有 9%。
这个结果让很多人第一次看会愣住。原因就在于那 0.999 的健康人基数太大了,1% 的假阳性率乘以 999 个人,产生了将近 10 个假阳性,而真阳性只有不到 1 个。假阳性在数量上压倒了真阳性。
这个数字的实践意义是明确的:对于低发病率的疾病,单项检测阳性的确诊价值有限,必须做复查或者结合其他指标。这也解释了为什么很多筛查流程是"初筛阳性再做一次确认检测",因为两次独立的假阳性同时发生,概率会降到万分之一,后验概率就上去了。这个"串联检测"的设计,本质上就是贝叶斯更新。
4.4 贝叶斯更新:一次不够就再来一次
假设第一次阳性之后,我们再做一次独立检测,结果还是阳性。这时候第一轮的结论 9% 就成了第二轮的先验。
P(患病|两次阳性) = (0.09 × 0.99) / (0.09 × 0.99 + 0.91 × 0.01) = 0.0891 / (0.0891 + 0.0091) ≈ 0.907
两次阳性之后,患病概率从 9% 跳到了 90% 以上。这就是贝叶斯更新的威力:每一条新证据都在修正信念,而且修正幅度取决于证据的判别力。
这个模式在工程上用得极多。垃圾邮件过滤里,每出现一个关键词就更新一次是垃圾邮件的概率;推荐系统里,用户每点一次就更新一次兴趣分布;故障定位里,每一条告警就更新一次各模块出问题的概率。它们都是同一个更新循环。
有一个细节值得注意:证据之间必须条件独立,否则重复更新会夸大置信度。两次检测如果用的是同一台设备、同一个试剂,误差是相关的,那么第二次阳性的信息量远小于第一次,"90%" 这个数就偏高了。这就是为什么临床上的确认检测通常换方法、换机构。
4.5 工程场景里它到底解决什么问题
垃圾邮件过滤是最经典的例子。假设历史数据里,垃圾邮件占 50%,正常邮件占 50%。"免费"这个词在垃圾邮件里出现的概率是 30%,在正常邮件里是 2%。现在一封邮件含"免费",它是垃圾邮件的概率是多少?
P(垃圾|免费) = (0.5 × 0.30) / (0.5 × 0.30 + 0.5 × 0.02) = 0.15 / 0.16 = 0.9375
93.75%。单个词就把判断推到了这个程度。朴素贝叶斯就是把这个计算对每个词做一遍然后连乘,虽然"特征独立"这个假设在自然语言里明显不成立,但因为只需要排序不需要精确概率,实际效果一直不错。
故障定位是另一个高价值场景。某系统有 5 个模块,历史故障分布是已知的先验。现在监控报了一条特定类型的告警,每个模块产生这条告警的概率可以从历史日志里统计。用贝叶斯一算,就能得到"这条告警最可能是哪个模块引起的"的排序,比人工翻日志快得多。这里的关键是似然矩阵要估得准,也就是每个模块产生每类告警的频率要统计充分。
冷启动场景里先验的作用尤其明显。新上线的商品没有任何点击数据,怎么估它的点击率?用同类目同价格带的平均点击率做先验,然后用后续的少量真实曝光做更新。这就是很多推荐系统里"贝叶斯平滑"的底层逻辑,本质上是用先验来抵消小样本的方差。
4.6 代码实现和数值稳定性
朴素贝叶斯的连乘在特征多的时候会出问题:几十个概率乘起来会小到浮点数表示不了,直接变成 0。标准解法是转到对数空间,把乘法变成加法。
import numpy as np def naive_bayes_log_space(prior, likelihoods): """ prior: dict, 各类别先验概率 {'spam': 0.5, 'ham': 0.5} likelihoods: dict, 每个类别下各特征出现的概率 {'spam': [0.30, 0.20], 'ham': [0.02, 0.05]} 返回:归一化后的后验概率 """ log_post = {} for cls in prior: log_p = np.log(prior[cls]) for p in likelihoods[cls]: log_p += np.log(p) log_post[cls] = log_p # log-sum-exp 归一化 max_log = max(log_post.values()) exp_vals = {k: np.exp(v - max_log) for k, v in log_post.items()} total = sum(exp_vals.values()) return {k: v / total for k, v in exp_vals.items()} prior = {'spam': 0.5, 'ham': 0.5} likelihoods = { 'spam': [0.30, 0.20, 0.15], # 三个词在垃圾邮件里的出现率 'ham': [0.02, 0.05, 0.10], # 同一批词在正常邮件里的出现率 } print(naive_bayes_log_space(prior, likelihoods))这段代码里最关键的不是循环,而是max_log那一步。直接对 log 值做 exp 可能溢出,先减去最大值再 exp,结果不变但数值稳定。这个技巧叫 log-sum-exp,是概率计算的标配,写一次以后到处能用。
还有一个配套问题是零概率。如果某个词在训练集里从未在垃圾邮件中出现过,likelihood 就是 0,取对数变成负无穷,整个判别直接失效。解法是拉普拉斯平滑,分子加 1、分母加类别数,让任何特征都有一个很小的非零概率。这一行改动看起来不起眼,但它决定了模型在遇到新词时是崩溃还是给出合理输出。
5. 踩坑记录与排查速查表
5.1 常见错误对照表
下面这张表是我这些年实际遇到过的、以及帮别人排查过的错误,按出现频率排序。建议在做任何概率相关的计算前扫一眼。
| 错误现象 | 根本原因 | 排查方法 | 修正方案 |
|---|---|---|---|
| 概率算出来大于 1 | 多个不互斥事件直接相加 | 检查事件之间是否有交集 | 用容斥原理,或改用联合概率 |
| 后验概率异常高 | 忽略了先验,只看似然 | 把先验代入重算一遍 | 补上 P(A) 项 |
| 结果比实际小一个数量级 | 把相关事件当独立相乘 | 算 P(A∩B) 对比 P(A)·P(B) | 改用条件概率链式展开 |
| 遇到新特征模型输出为 0 | 零概率导致对数下溢 | 打印 likelihood 看是否有 0 | 加拉普拉斯平滑 |
| 多轮更新后过度自信 | 证据之间不独立被重复计数 | 检查特征相关性矩阵 | 去重特征或降低权重 |
| 分组概率之和不为 1 | 分组不满足互斥完备 | 检查是否有遗漏类别 | 补齐划分或加"其他"类 |
表里的每一行背后都有真实的返工成本。尤其是第三行和第五行,前者会让整个策略偏保守,后者会让模型在线上表现得比离线评估激进得多。
5.2 数值下溢和精度损失的实战处理
除了对数空间,还有一个更隐蔽的问题:极端小的先验会被浮点精度吃掉。比如 P(A)=1e-9,直接参与计算可能没问题,但如果和其他量做加减,就会被舍入误差影响。处理方式是全程在 log 空间做加减,只在最后需要输出概率时才 exp 回来。
另一个实操细节是归一化常数的计算顺序。用全概率公式算 P(B) 时,如果分组很多,先累加再取倒数比先取倒数再累加精度更好。这个差别在分组上百的时候会显现出来。
注意:不要用
float32做概率连乘,特征数超过 20 个就很容易下溢。默认用float64,如果还不行就转 log。
5.3 独立性假设被滥用的后果
朴素贝叶斯之所以"朴素",就是因为它假设所有特征在给定类别下条件独立。这个假设在现实中几乎从不成立,但模型效果往往还行,原因是分类任务只要求排序正确,不需要概率值准确。
但这不意味着可以随便用。一旦你的业务需要的是准确的概率值而不仅仅是排序,比如要做期望收益计算、要做风险定价,那独立性假设带来的偏差就会直接变成钱上的损失。判断方法很简单:拿模型输出的概率和真实频率做校准曲线,如果曲线明显偏离对角线,说明概率被高估或低估了,这时候要么去相关特征,要么做后处理校准(比如保序回归)。
5.4 先验到底怎么定才不算拍脑袋
先验是贝叶斯方法被质疑最多的地方,但工程上有几种可靠的做法。
用历史数据做先验是最常见的。某个功能的点击率历史稳定在 3% 左右,那就用 3% 做先验,然后用新数据更新。这种做法在数据量足够时几乎无争议。
用更大范围的同类数据做先验适合冷启动。新上线的商品没有数据,就用同类目的整体分布做先验。这里要注意粒度,太粗先验没用,太细又没数据。
用无信息先验适合完全没有把握的场景。均匀分布或者 Beta(1,1) 都是常见选择,效果是让数据说话,先验只是让计算能启动。
用专家判断做先验要谨慎,但也不是不能用。关键是把它当作一个可以随着数据积累逐渐被"洗掉"的初始值,如果数据和先验冲突,要能识别出来并调整,而不是硬扛。
我个人习惯是,先验只用来解决"零数据"和"小样本"这两类问题,数据量上来之后逐步降低先验权重,让经验分布接管。这套做法在多业务线并行的团队里比较好落地,因为它不要求先验一开始就准,只要求它能被修正。
6. 我个人的一点使用体会
这几个公式我用了很多年,最大的感受是,它们的价值不在于计算本身,而在于逼你把问题问清楚。动手算之前必须回答三个问题:我现在已知什么、我要预测什么、这两个量之间是什么方向的关系。把这三问回答清楚,公式套哪个基本就定了,剩下的都是算术。
还有一个习惯我觉得挺有用:不管多简单的概率题,先用模拟跑一遍。三五十行代码,一秒钟出结果,比在纸上推半小时还容易发现自己假设错在哪。上面那段抽样的代码,我改一改就能拿去验证任何一个联合概率的直觉,这个成本几乎为零,收益是把错误挡在提交之前。
最后一个提醒是,别把贝叶斯法则当成万能工具。它在"证据可靠、先验可估、样本独立"这三个条件都满足时威力最大,缺一个就要打折。尤其是证据的相关性,这一条最容易在工程里被忽略,也最容易导致模型上线后过度自信。看到后验概率高得离谱的时候,先别高兴,回去查一查你的证据是不是重复计数了。