武辰作品索引

知乎作品库

回答 · 1

知乎原文

有哪些“真懂和真不懂的都是A,半懂不懂的才是B”的例子?

这种情况,当然要把洛必达和概率论请过来。

先来一道洛必达。

lim⁡x→+∞sin⁡xx2\displaystyle\lim_{x\to+\infty}\frac{\sin x}{x^2}

刚学洛必达的人表示:“分子分母分别求导。上面是 cos⁡x\cos x ,下面是 2x2x ,答案是 0。”

学霸说:“等一下,洛必达是拿来算 0/00/0 和 ∞/∞\infty/\infty 的。但这个 sin⁡x\sin x ,既不趋于 0,也不趋于无穷。你这一步不能用。”

学神幽幽来了一句:“还真能用洛必达。”

更一般的洛必达法则里,分母趋于无穷时,没要求分子也趋于无穷,甚至没要求分子有极限。 这题分母的导数 2x2x 不为零,求导后的比值 cos⁡x/(2x)\cos x/(2x) 又确实趋于 0,所以第一步就能这么算。

这题用夹逼也能一眼看出来。


再来一道抛硬币。

有两枚硬币,每一枚抛出正面的概率都是一半。正面记作 1,反面记作 0。现在只告诉你,这两个结果的相关系数是 0。问:两枚都是正面的概率是多少?

初学者:“不相关不就是你抛你的、我抛我的?可以根据独立性乘法公式:1/2乘1/2,四分之一。”

学霸说:“不相关 推不出 独立。你怎么能直接乘?这题条件不够。”

学神表示:“二元离散随机变量中,不相关和独立还真是等价的。”


再把刚才的问题反过来。

只告诉你两个随机变量相互独立,你能不能直接写:它们的相关系数是 0?

初学者:“这怎么知道?你连这两个变量是什么都没告诉我。”

学霸说:“用不着告诉我。独立就不相关,这不是课本上刚讲过的吗?”

学神说:“那我给你两个独立的柯西变量,你算一个给我看看。”

两个变量的方差都有限、都不为零的时候,独立确实能推出相关系数是 0。但柯西变量连数学期望都没有,相关系数根本没法定义。

知乎原文

有哪些值得推荐的《概率论》教材或者参考书?

毛遂自荐,我写了一本可以自学的概率论教材。

另外,还有三本书是我自己读过、也愿意推荐的:

茆诗松等的《概率论与数理统计教程》。我自己在本科考研的时候用的就是这本书,例题习题很多。如果你想按大学课程的顺序学一遍,边看书边做题,我推荐这一本。

陈希孺的《概率论与数理统计》。我非常喜欢它讲概念的方式。有时候他会从哲学的角度去讲,醍醐灌顶。

Casella 和 Berger 的《统计推断》。这本更偏数理统计。学过基础概率之后,如果想系统学一遍参数估计、置信区间和假设检验这些内容,我推荐这本。


应该有不少关注我的朋友知道,我高中是文科生,但一直对数学感兴趣,参加过数学竞赛。那时候经常刷Matrix67大神的博客(好像也是文科出身?),里面很多数学问题我都很感兴趣。

本科我在武大读金融,研究生转到数学院读数学,毕业之后做AI算法工作。

我大学其实很少去听课,很多东西都是靠自学的。自学的时候我发现,很多数学教材写得太简略了,很喜欢跳步,不适合自学。同时我也发现,概率论里很多知识是可以串起来的,可以从直觉的角度去理解。

当时我心里就燃起一团火,想把我对概率论的这些直观理解分享给大家。那时候没敢想写教材,只是在知乎上写文章、写回答。没想到一分享就挺火,几万的浏览,几千的点赞。从普通的概率问题,到很有争议的三门问题,我都写过,后来也成了知乎数学领域的优秀回答者。

我就在想,如果有一本教材,能把概率论的世界完完整整地展现给非数学专业的本科生,甚至有微积分基础的高中生,那该多好。倒不是说要把它讲得很浅,而是我发现,概率论里很多东西是可以直观理解的,不同的知识点之间可以有机串起来。但好像没有哪本教材,把这些完完整整写下来,交给学生。

举个例子,传统教材讲二项分布、泊松分布、几何分布,往往是一个一个孤立地讲。但事实上它们之间都有联系。一旦掌握了这些联系,看这些分布的视角就会大不一样,甚至看这个世界的视角都会不一样。

再举个例子,几何分布和指数分布的无记忆性,我觉得是初等概率里最有灵魂的性质,它能解释生活中很多问题。我在书里花了大量篇幅讲它:不只是这个性质怎么推出来的,还有为什么会有这个性质,几何分布和指数分布之间是什么关系,无记忆性又会带来什么结果。这些都是我当年学概率论时灵光一闪的思考。

无记忆性既符合直觉,又不符合直觉。我觉得学概率论真的要从直觉出发。不从直觉出发,很多结论就只能死记硬背;从直觉出发,再看这些结论,就像是降维打击。

后来工作了,更忙了,但我也没停下知乎和B站的更新。而且我发现,工作中用到的AI,底层很多都是概率论。交叉熵、softmax、贝叶斯公式、条件概率,到处都是。我越来越觉得,我真的要写这么一本书了。它不只是我学生时代的夙愿,也跟我的工作紧密相关。所以写书的时候,我也有意加了AI相关的内容。

至于怎么写,我还有一个想法。其实有些老师讲课讲得很好,我就想,如果把这些老师上课的录音转成文字,变成课本,会不会有很多人喜欢?

这也是我写这本书的方法。我对着手机录音,把内容口述出来,想象自己在上课,或者坐在学弟学妹旁边辅导他们。讲完再用AI转成文字,然后修改、润色。

交稿之前,其实有两位朋友建议我,别把自己是文科生这件事说出来。但我还是想如实讲讲自己的经历。

从研究生时辅导学生,到工作后做讲座、做分享、做教学,大家给我的评价基本都是"通俗易懂"。如果说我有什么优点,那这算一个。我从文科生到金融,到数学,再到算法,一路都是自学过来的。正因为走过这条路,我才知道自学会卡在哪里。

写书的过程中,我也不断把样章放出来,放在知乎上,放在读者群里,大家提了很多建议。我很感谢这些提建议的人。我想把它写成一本看了不卡壳的书,一本不依赖老师也能自学的书。

如果你问这本书有什么缺点,有位读者说得很中肯:我太"既要又要还要"了。

这本书700多页,是普通书的两倍厚度。里面有普通大学教材里的概率论,有考研的内容,有大厂AI算法岗的面试题,有考试不考的伽马分布,有很有争议的三门问题、信封问题、星期二男孩问题和辛普森悖论,还有AI的内容,甚至麻将、三国杀、狼人杀、炒股也都提了一嘴。像个大杂烩。

说实话,我写的时候就是这种心态,总想把我想要的东西全都倒出来。这可能会让一些读者迷失方向。比如你急着考研,会发现这本书太厚了。

还有读者说,这本书有点啰嗦。这个我也承认。我真的很怕读者读不懂,所以宁可啰嗦一点,多写几个字、多说几句话,也不想跳步。

当然,更多的反馈是正面的,这里就不一一念出来了。看过我发的样章、看过我以前视频的朋友,相信心里都有自己的判断。

说到底,这本书在某种意义上,就是写给十年前那个自学的自己的。不是为了考多高的分,也不是为了速成,而是真的想从直觉出发,把整个概率论学透。

这十年来我做教学、做分享、做演讲,每次看到台下的人听懂了、恍然大悟的那一刻,说实话,我是真的爽。我很享受把别人教会的感觉。

所以,如果这本书能让很多人觉得终于把概率论弄明白了,或者读得很爽,那我会非常开心。

我之前在知乎发过不少样章,下面也附了《大白话概率论:从数学直觉到AI视野》的目录。大家可以先看看样章和目录,再决定这本书适不适合自己。


为什么我国的概率与统计学教科书里不怎么讲幂律分布?
原文配图
原文配图
原文配图
原文配图

知乎原文

同一个prompt重复多次输入进 LLM,为啥得到的输出都不一样?

如果是按概率抽词,同一个问题问两遍,抽到的词当然可能不一样。比如说你问中国的首都在哪里,大模型会对所有的词都赋予一个概率,比如说给北京的概率是0.99,给长安的概率是0.0001,给深圳的概率是0.00000001。它就这么按概率去抽。其实这个例子倒不是那么合适,因为它基本上能够以99%的概率抽到正确答案。很多时候,概率最高的那个词都不会超过0.1。比如说,这个人长得真,后面接漂亮也好、接美丽也好、接俊俏也好,它都有一定的概率,但每个的概率都不是特别高(所有的可能性加起来不能超过1)。所以你第一遍让它生成的时候,可能会看到俊俏,第二遍可能则看到的是美丽。

这里有两个细节。

第一个细节,它到底是怎么赋予概率的?里面用到了 Softmax 。

我们首先有一个词汇表的概念。一般来说,大模型的词汇表可能有几十万个或者上百万个。它就相当于我们的新华字典。它是组成我们语言文字的最基本的元素。我们在这里就以3个为例,我们假设大模型的词汇表的大小只有3个词汇,这是一个非常简陋的假设,这里是为了方便讲解。

我们把“今天天气真”输入给大模型之后,大模型就要为词汇表中的每一个词去赋予一个分数。这个分数用行话来说就叫 logits。举个例子,比如说“好”这里得到了 2.0 分,“的”得到了 1.0 分,“不”得到了 -1.0 分。

比如说我们可以一眼看出,“好”这个字它的分数是最大的,那我们就可以理解成,模型接下来“今天天气真”后面输出“好”的概率是最大的,就是这么一个意思。但是这样还不够直观。我们最好还是要把它转成一个概率。转成一个概率之后,大模型在输出下一个词的时候就有章可循,可以根据概率去采样了。

那现在这个分数能不能直接作为概率?肯定不行,因为概率是有取值范围的,它的取值范围是 0 到 1。那我这里又是大于 1 的,又是负数的,概率怎么可能是负数呢?所以 logits 不能直接当概率。它必须要满足两个条件:第一个就是它的取值是 0 到 1,第二个就是所有概率的取值加起来应该等于一。

这里两个条件都不满足。你看它加起来,2 加 1 加 -1,它等于 2,它不等于一。那怎么办呢?有一个很自然的想法,就是我先加一遍求和,然后每个数除以求和,行不行?

它们求和等于 2,那么 2 除以 2 等于 1,1 除以 2 等于 1/2,然后 -1 除以 2 等于 -1/2。你看它加起来是不是等于一了?1 加 1/2 加 -1/2,加起来确实等于一了,但是它还是出现了负数,概率是不能出现负数的。

所以我们要怎么样把这一组 logits 变成概率呢?这个时候我们就要把这个指数函数请出来了。指数函数它有一个很好的性质:不管 x 取什么值,我们都发现,它的指数函数永远是大于零的,它不可能在 X 轴下面。

所以说我们的 Softmax 就出来了。它具体是怎么做的呢?还是 2、1、-1 举个例子。2 的话,先把它变成 e 的 2 次方;1 呢,就变成 e 的 1 次方;这里的 -1 就变成 e 的负 1 次方,做一个指数变换。

指数变换之后,我们再结合刚刚求和的例子。我们刚刚是不是把这三个词的分数加起来,再拿总和当分母?其实这个思想是正确的,我们这里用同样的思想:

“好”:e² ÷ (e² + e¹ + e⁻¹)

“的”:e¹ ÷ (e² + e¹ + e⁻¹)

“不”:e⁻¹ ÷ (e² + e¹ + e⁻¹)

这就是完整的 Softmax 的变换。这样算出来的三个数,能不能称为概率呢?我们来看它是不是满足性质。它们加起来是不是等于一?显然它们加起来是等于一的,而且每一个数字都处于 0 到 1 的范围。

我们算一遍,比如说 e 的平方等于 7.389,e 的 1 次方等于 2.718,e 的负 1 次方等于 0.368。然后我们这么去算一遍,发现它约等于 0.705,第二个约等于 0.26,第三个约等于 0.035。

原文配图
Softmax 的三个步骤:原始分数、取指数、除以总和得到概率(图源:作者大模型书稿)。


也就是说,logits 只是神经网络模型的一个输出,它不能当作概率。经过 Softmax 的变换,就能够称之为概率了。

第二个细节就是温度这个参数。温度设成 0 时,通常就不抽了,直接选分数最高的那个词。

如果说温度为 0,那么是不是能保证同样的输入输给大模型,两次的结果是一样的呢?一般情况下是这样的,但是也不一定,因为大模型底层的浮点数加法不一定满足结合律,即 (A+B)+C 不一定等于 A+(B+C)。

为什么会是这样?我们举个很简单的例子,比如说 A 等于 0.0004,B 等于 0.0005,C 等于 0.0006。我们假设每做一次运算,都只能四舍五入到小数点后三位。

如果我们先算 A 加 B,然后再加上 C,A 加 B 等于 0.0009,四舍五入变成 0.001,然后再加上 C 等于 0.0016,四舍五入变成 0.002。

但是如果先算 B 加 C,等于 0.0011,四舍五入变成 0.001,然后再加上 A,结果是 0.0014,四舍五入变成 0.001。所以你可以看到,不同的相加顺序,最终的结果是不同的。一个是 0.002,一个是 0.001。

原文配图
每一步都四舍五入到小数点后三位:换个相加顺序,结果从 0.002 变成了 0.001。

不同的计算框架、不同的计算方式,有时候相加的顺序就会不同。这个微小的差别继续往后传,就有可能影响最后选出来的词。所以温度为 0,也不一定能保证每次的输出都一模一样。

知乎原文

如何通俗理解 beta 分布?

概率本身有没有概率?这就是贝塔分布要回答的事情。

以下内容节选自《大白话概率论》一书。

贝塔分布(概率的概率)

前面的章节里,不管是扔骰子还是量身高,我们都默认手里那把“尺子”(概率 pp)是固定的。例如硬币的 p=0.5p=0.5。但在现实世界里,最让人头秃的往往不是“尺子量出第一种情况了多少”,而是“这把尺子本身准不准”。贝塔分布,就是专门用来衡量“尺子”的尺子。

从一个最朴实的问题开始。假设一位朋友送你一枚硬币,但这可不是一枚普通的硬币。我们知道,一枚普通的、物理上均匀的硬币,掷出正面的概率 pp 大约是 0.50.5。然而,朋友神秘兮兮地告诉你:“这硬币是我从一个奇怪的地方找到的,我完全不知道它是不是均匀的,甚至不知道它有没有被做过手脚。”你现在想知道:掷出正面的概率 pp 究竟是多少?它可能接近 00(总是反面),接近 11(总是正面),还是在中间的某个值?最重要的是,你有多确定你的答案?

你可能会立刻想到二项分布。是的,如果我知道 pp,我就能用二项分布算出掷 nn 次出现 kk 次正面的概率。但是,仔细想想我们此刻真正的处境:二项分布能回答我的问题吗?

二项分布是上帝视角的产物:上帝知道 p=0.5p=0.5,所以他知道扔 10 次大概会有 5 次正面。本节要讲的贝塔分布是人类视角的产物:人类只能看到“10 次里有 7 次正面”,然后战战兢兢地去反推:上帝手里那个 pp 到底是多少?

pp 是一个介于 00 和 11 之间的数。但我们面对的困境是:它不是一个固定的、已知的参数;它本身就是一个我们想要去估计、去描绘其不确定性的 未知量。我们不仅想知道“pp 最可能是多少”,还想知道“pp 是 0.60.6 的可能性,比它是 0.50.5 的可能性大多少?”以及“pp 有没有可能是 0.90.9?”

换句话说,我们需要一种数学工具,来对整个区间 [0,1][0,\ 1] 上的每一个可能的 pp 值,分配一个“可信度”。我们需要一个定义在 [0,1][0,\ 1] 上的概率分布,但这个分布描述的不是事件发生的次数,而是 那个驱动着事件发生的根本概率本身的可能性。

这听起来确实有点像绕口令。换个说法:pp 现在不是一个数,而是一个“进度条”上的滑块。贝塔分布就是告诉你,这个滑块停在 0.50.5 处的可能性有多大?停在 0.90.9 的可能性又有多大?它给进度条上的每一个刻度都打了一个“可信分”。

1. 从“先入之见”到“眼见为实”:一个思想的实验

先忘掉公式。面对那枚神秘的硬币,在抛掷之前,你心里对 pp 会不会有一些最初步的、模糊的猜想?

第一种情况:你没有任何先入之见。 你对这枚硬币一无所知,除了知道它是一个物理上可以抛出两种结果的物体。那么在你心里,p=0.1p = 0.1(几乎总是反面)的可能性,和 p=0.5p = 0.5(完全公平)的可能性,和 p=0.9p = 0.9(几乎总是正面)的可能性,应该是一样大的。因为你没有任何理由偏爱其中任何一个值。你如何描绘这种“完全无知”的信念呢?一条从 00 到 11 的、高度相等的水平线,再合适不过了(如图1 中的灰色水平线)。是的,这就是我们学过的 [0,1][0,1]上的均匀分布。

第二种情况:你有了一些初步的经验或猜想。 例如,你朋友虽然记不清,但嘟囔了一句“我记得好像用它打赌时,正面出现得更多,但偶尔也有反面”。那么,在你心里,pp 在 0.70.7 到 0.80.8 之间的可能性,就会比它在 0.050.05 附近或 0.950.95 附近的可能性要大。你会画出一条在 0.80.8 左右鼓起来,向两边逐渐下降的、像小山丘一样的曲线(如图1 中的蓝线)。pp 离 0.80.8 越远,你认为它真实的可能性就越小。

第三种情况:爱恨分明型。 这就好比网上对榴莲的评价。爱吃的人极爱(p≈1p \approx 1),恨的人极恨(p≈0p \approx 0),唯独没几个人觉得“还行吧”(p≈0.5p \approx 0.5)。这种两极分化的信念,画出来就是一个大写的 U(不同先验信念对应的贝塔分布如图1所示)。

我们其实已经在用一条定义在 [0,1][0,1] 上的曲线,来描述对一个未知概率的信任程度了。贝塔分布,就是给这类曲线取的一个统一名字,并提供了一套简单而强大的“调控装置”,让你能精确地生成从“完全无知”到“强烈倾向”,从“单峰”到“U形”的任何一种信念曲线。这套装置,就是两个参数:α\alpha (alpha) 和 β\beta (beta)。


原文配图
图1 不同先验信念对应的贝塔分布



2. 贝塔分布的形状:“伪计数”的魔术

描述对概率 pp 的信念,可以是一条定义在 [0,1][0,1] 上的曲线。贝塔分布就是用来画这种曲线的数学工具,它由两个正参数 α\alpha 和 β\beta 控制,记作 Beta(α,β)\text{Beta}(\alpha,\ \beta)。

这两个参数的直观意义是什么?回想一下二项分布:如果我们知道成功概率 pp,那么在 nn 次独立试验中看到 kk 次成功的似然(或者说,“合理程度”)与 pk(1−p)n−kp^k (1-p)^{n-k} 成正比。指数 kk 和 n−kn-k 就是观测到的成功与失败的计数。

贝塔分布的概率密度函数 f(p)∝pα−1(1−p)β−1f(p) \propto p^{\alpha-1}(1-p)^{\beta-1} 有着极其相似的形式。这强烈暗示我们,参数 α\alpha 和 β\beta 应该与“计数”有关。

这里采用一种最直观的解释,如下。

  • 把 α\alpha 直接看作是 “伪成功次数”。
  • 把 β\beta 直接看作是 “伪失败次数”。
  • 因此,总的“伪实验次数”就是 α+β\alpha + \beta。

你完全可以把 α\alpha 和 β\beta 看作是你的“预设经验值”。在开始扔硬币之前,你脑子里可能已经装了一些偏见。α\alpha 就是你脑补出来的正面次数,β\beta 就是脑补的反面次数。贝塔分布的数学魔术就在于:它允许你把脑补的次数和真实看到的次数直接相加。

在贝叶斯更新的思想中,这个解释特别简洁: 如果你的先验是 Beta(α,β)\text{Beta}(\alpha, \beta)(代表 α\alpha 次伪成功,β\beta 次伪失败),然后你观测到 kk 次真实成功和 mm 次真实失败,那么你的后验信念就是 后验=Beta(α+k,β+m)\text{后验} = \text{Beta}(\alpha + k,\ \beta + m) 你只需要做加法:把真实的计数加到对应的伪计数上即可。

有些教材中会将 (α−1)(\alpha-1) 和 (β−1)(\beta-1) 解释为伪计数,这源于贝塔分布密度函数中指数的形式。两种解释在数学上等价,只是对“零先验”状态的设定不同。本书采用 α\alpha、β\beta 直接作为伪计数的解释,因为它让贝叶斯更新的公式——简单相加——显得最为直观。

图2展示了不同参数下贝塔分布的形状。


原文配图
图2 不同参数下贝塔分布的形状



  • 当 α=1\alpha=1, β=1\beta=1,即Beta(1,1)\text{Beta}(1,1)。在我们的“伪计数”账本里,这意味着你带着“1次伪成功和1次伪失败”的初始本金入场。为什么要有这笔初始本金?你可以把它看作是贝叶斯游戏的一张“最低门票”。在数学上,如果没有任何本金(α=0,β=0\alpha=0, \beta=0),公式就会崩溃(分母为0)。为了让游戏能进行下去,系统默认给了每个选项 1 票。
    虽然账本上有了这 1 胜 1 负的底薪,但这仅仅是为了兜底,并不代表你真的在现实中获得了倾向性的经验。反映在贝塔分布的核心公式 pα−1(1−p)β−1p^{\alpha-1}(1-p)^{\beta-1} 中,此时的指数恰好都是 0,整个式子变成了一个常数 1。因此,它的图纸上呈现出一条毫无波澜的水平线,不同参数下贝塔分布的形状如图2所示。这正是 [0,1][0,1] 上的均匀分布,代表了“没有任何偏好”的初始无知状态。
  • 当 α>1\alpha>1, β>1\beta>1,且两者相等:例如 α=β=5\alpha=\beta=5。这好比你在想象中,已经掷了 1010 次,其中成功和失败各 55 次。那么你的信念会以 0.50.5 为中心,对称地集中起来。分布图像是一个漂亮的、对称的钟形山丘。α\alpha 和 β\beta 越大,这个山丘就越瘦、越高,表示你的信念越集中、越确定。此时pp的分布是Beta(5,5)\text{Beta}(5,5)。
    这特别符合直觉:如果你在想象中只掷了 1010 次硬币,其中 55 次正面 55 次反面,你虽然会猜测这枚硬币可能是均匀的(pp 的期望是 0.50.5),但心里并不十分有底——也许下一次投掷就会明显偏离这个比例。这种不确定性体现在图像上,就是一个相对“矮胖”的钟形山丘,意味着 pp 在 0.50.5 附近的一个较宽范围内都有相当的可能性。但如果你在想象中已经掷了 5050 次,其中 2525 次正面 2525 次反面,那么你几乎可以肯定这枚硬币就是均匀的(或极其接近均匀),此时你的信念曲线会变得非常“尖瘦”,紧紧地集中在 0.50.5 附近,表示你对 p=0.5p = 0.5 这个判断有极强的信心。此时pp的分布是Beta(25,25)\text{Beta}(25,25)。信念确定性的对比如图3所示,Beta(5,5)\text{Beta}(5,5) 分布表示基于少量数据的信念,曲线较宽;Beta(25,25)\text{Beta}(25,25) 分布表示基于大量数据的信念,高度集中在 p=0.5p=0.5 附近。
原文配图
图3 信念确定性的对比


  • 当 α>1\alpha>1, β>1\beta>1,但两者不等:例如 α=5\alpha=5, β=2\beta=2。这相当于你想象中成功更多些,所以分布的山丘峰值会向右偏移,超过 0.50.5。此时 pp 的后验均值是 5/(5+2)≈0.7145/(5+2) \approx 0.714,而众数(峰值位置)是 (5−1)/(5+2−2)=0.8(5-1)/(5+2-2)=0.8。此时pp的分布是Beta(5,2)\text{Beta}(5,2)。
  • 当 α<1\alpha<1, β<1\beta<1:这是非常有趣的情况。我们刚刚还说 α\alpha 和 β\beta 像是“伪计数”,现在它们小于 11,这难道意味着我们见过“不到一次”的实验?这听起来有点奇怪。让我们换个角度思考。
    想象这样一种场景:你在研究一种罕见的生物变异现象。一位老科学家告诉你:“根据我的经验,这种变异要么几乎不发生(概率极低),要么一旦条件成熟就几乎必然发生(概率极高),但我几乎没见过它表现出中等概率的情况。” 或者想想某些“要么全有,要么全无”的现象:例如某种化学催化剂——它要么极其有效,使得反应几乎100%成功;要么完全失效,让反应几乎从不发生。那些“效果平平”的催化剂反而少见。 这种信念反映在数学上,就是我们认为 pp 接近 00 或接近 11 的可能性比较大,而中间值的可能性比较小。这对应着 α<1\alpha<1,β<1\beta<1 的情况。
    例如 α=β=0.5\alpha=\beta=0.5。画出来的图是一个 U形曲线!它恰如其分地描述了那种“事情要么很容易成功,要么很难成功,但不太可能是平平无奇的”的信念。这种看似极端的信念在统计学中却有重要地位:Jeffreys 先验(一种经典的非信息先验)正是 Beta(0.5,0.5)\text{Beta}(0.5,\ 0.5)。
    此时 pp 的分布是 Beta(0.5,0.5)\text{Beta}(0.5,0.5),它的概率密度函数在 pp 接近 00 或 11 时会变得很大,而在中间区域则比较小。

3. 贝塔分布公式:拆解“信念的构成”

来看它的标准模样。贝塔分布的概率密度函数(PDF)是 f(p;α,β)=pα−1(1−p)β−1B(α,β),其中 0≤p≤1,α>0,β>0f(p;\ \alpha,\ \beta) = \frac{ p^{\alpha-1} (1-p)^{\beta-1} }{ B(\alpha,\ \beta) }, \quad \text{其中 } 0 \le p \le 1,\ \alpha>0,\ \beta>0

把它拆解看看。

  1. 核心部分。pα−1(1−p)β−1p^{\alpha-1} (1-p)^{\beta-1} 这一部分承载了所有的 “信念逻辑”。 pp 是成功的概率,(1−p)(1-p) 是失败的概率。
    你可能注意到了:指数是 α−1\alpha-1 和 β−1\beta-1,而不是我们刚刚定义的伪计数 α\alpha 和 β\beta。这似乎有个“11”的偏差。为什么? 这里的指数 α−1\alpha-1 和 β−1\beta-1,在贝叶斯框架下是为了数学的自洽性和边界情况的合理性。例如当 α=β=1\alpha=\beta=1(即我们认为自己有 11 次伪成功和 11 次伪失败)时,密度函数变为 p0(1−p)0=1p^{0}(1-p)^{0}=1,恰好是均匀分布——这对应了“一次成功和一次失败相互抵消,不提供任何倾向性信息”的直观。
  2. 归一化部分。B(α,β)B(\alpha,\ \beta) 这是贝塔函数(Beta Function)。它是干什么的?它是一个 保证总概率为 11 的“调节器”。
    因为核心部分 pα−1(1−p)β−1p^{\alpha-1} (1-p)^{\beta-1} 在 [0,1][0,1] 上积分不一定等于 11。我们需要除以一个常数,让整个曲线下的面积恰好是 11,这才是一个合法的概率密度函数。B(α,β)B(\alpha,\ \beta) 就是这个常数。 它的具体形式是 B(α,β)=∫01tα−1(1−t)β−1dtB(\alpha,\ \beta) = \int_0^1 t^{\alpha-1}(1-t)^{\beta-1} dt。看到没?和概率密度函数的分子结构很像。
    它还可以用伽马函数表示为 B(α,β)=Γ(α)Γ(β)Γ(α+β)B(\alpha,\ \beta) = \frac{\Gamma(\alpha)\Gamma(\beta)}{\Gamma(\alpha+\beta)}。这把贝塔分布和我们刚学过的伽马分布联系了起来。不过,如果你第一次接触,暂时可以就把 B(α,β)B(\alpha,\ \beta) 看作一个计算好的、使公式成立的常数即可。计算它的具体值,是计算机和积分表的工作。

4. 共轭性——信念如何更新

当有了真实的实验数据后,我们如何更新对 pp 的信念?这是贝塔分布最漂亮的地方。

假设我们关于 pp 的 先验信念 是 Beta(α,β)\text{Beta}(\alpha,\ \beta)。然后我们进行了一组真实的实验:做了 nn 次独立伯努利试验(例如掷了 nn 次硬币),观察到了 kk 次成功和 n−kn-k 次失败。

根据贝叶斯的思想:新信念(后验) ∝\propto 似然函数 ×\times 旧信念(先验)。

  • 似然函数是什么?在给定 pp 时,看到这组数据的概率。这正是 二项分布 的概率核:Likelihood∝pk(1−p)n−k\text{Likelihood} \propto p^k (1-p)^{n-k}。(忽略了组合数常数)
  • 先验分布是:Prior∝pα−1(1−p)β−1\text{Prior} \propto p^{\alpha-1} (1-p)^{\beta-1}。

把它们乘起来,有 后验分布∝[pk(1−p)n−k]×[pα−1(1−p)β−1]=pα+k−1(1−p)β+n−k−1\text{后验分布} \propto \left[ p^k (1-p)^{n-k} \right] \times \left[ p^{\alpha-1} (1-p)^{\beta-1} \right] = p^{\alpha+k-1} (1-p)^{\beta+n-k-1}

仔细看这个结果——它的形式和贝塔分布的核心部分 pα−1(1−p)β−1p^{\alpha-1} (1-p)^{\beta-1} 一模一样,只是指数变成了 (α+k−1)(\alpha+k-1) 和 (β+n−k−1)(\beta+n-k-1)。

这意味着什么?这意味着,后验分布也是一个贝塔分布。 后验分布=Beta(αnew,βnew)=Beta(α+k,β+n−k)\text{后验分布} = \text{Beta}(\alpha_{\text{new}},\ \beta_{\text{new}}) = \text{Beta}(\alpha + k,\ \beta + n - k)

这个性质被称为共轭性。贝塔分布是二项分布(和伯努利分布)的 共轭先验。 它的潜台词是:“原本是什么形状,更新完还是什么形状”。这意味着你永远不需要重新选择分布族,只要不断更新两个数字就够了。

贝叶斯更新示例如图4所示,你只需要把观察到的成功数 kk 加到伪成功数 α\alpha 上,把观察到的失败数 n−kn-k 加到伪失败数 β\beta 上,就得到了更新后的、包含了所有信息的后验分布。


原文配图
图4 贝叶斯更新示例



例1

假设你对一枚硬币的初始信念是 Beta(5,2)\text{Beta}(5,\ 2)(即你认为正面概率 pp 大概在 5/7≈0.7145/7 \approx 0.714 附近)。然后你实际掷了 1010 次硬币,结果出现了 33 次正面,77 次反面。你的信念将如何更新?

解答

这是一个典型的贝塔分布共轭更新过程。

  • 先验分布:Beta(α=5,β=2)\text{Beta}(\alpha=5,\ \beta=2)。
  • 观测数据:成功数 k=3k=3,失败数 n−k=7n-k=7,总试验数 n=10n=10。
  • 后验分布:Beta(αnew,βnew)=Beta(5+3,2+7)=Beta(8,9)\text{Beta}(\alpha_{\text{new}},\ \beta_{\text{new}}) = \text{Beta}(5+3,\ 2+7) = \text{Beta}(8,\ 9)。

更新后,你对 pp 的期望值从 5/7≈0.7145/7 \approx 0.714 变成了 8/17≈0.4718/17 \approx 0.471。数据(实际正面率 0.30.3)把你的信念从高估成功的方向,有力地拉了回来。同时,α+β\alpha+\beta 的总和从 77 增加到了 1717,信念的“山峰”变得更尖、更集中了,因为有了更多数据,我们更确定了。

贝塔分布的参数,就像是你知识账户里的“记忆单元”。α\alpha 记录着(真实和虚拟的)总成功次数,β\beta 记录着总失败次数。每获得一次新的数据,就在对应的账户上加一。 你的整个知识状态,就由这两个数完全刻画了。

例2

一位篮球爱好者自称投篮很准。基于他之前的自我介绍,你对他的投篮命中率 pp 有一个初步的估计,用 Beta(5,2)\text{Beta}(5, 2) 分布描述(即你认为他的命中率大概在 5/7≈0.7145/7 \approx 0.714 附近,但还不太确定)。某天你观察他练习,他投篮 1010 次,命中 44 次。

  1. 此时你对他的命中率 pp 的信念应该如何更新?
  2. 一年后,他声称自己经过刻苦训练,水平大有提高。你再次观察他投篮 1010 次,这次他命中了 99 次。请问,结合两次观察,你现在对他的命中率 pp 的信念是怎样的?

解答

  1. 第一问的更新如下。
  • 先验分布:Beta(α=5,β=2)\text{Beta}(\alpha=5,\ \beta=2)。
  • 第一次观测数据:成功数 k1=4k_1=4,失败数 10−4=610-4=6,总试验数 n1=10n_1=10。
  • 后验分布(第一次更新后):Beta(α1,β1)=Beta(5+4,2+6)=Beta(9,8)\text{Beta}(\alpha_1,\ \beta_1) = \text{Beta}(5+4,\ 2+6) = \text{Beta}(9,\ 8)。

更新后,你对 pp 的期望值从 5/7≈0.7145/7 \approx 0.714 变为 9/17≈0.5299/17 \approx 0.529。因为实际观测的命中率(0.40.4)低于先验的期望,所以你的信念向下调整了。同时,α+β\alpha+\beta 从 77 增加到 1717,信念的“山峰”变得更尖一些,表示你对他命中率的确定性增加了。

  1. 第二问的更新如下。
  • 此时的先验分布是第一次更新后的后验:Beta(α1=9,β1=8)\text{Beta}(\alpha_1=9,\ \beta_1=8)。
  • 第二次观测数据:成功数 k2=9k_2=9,失败数 10−9=110-9=1,总试验数 n2=10n_2=10。
  • 后验分布(第二次更新后):Beta(α2,β2)=Beta(9+9,8+1)=Beta(18,9)\text{Beta}(\alpha_2,\ \beta_2) = \text{Beta}(9+9,\ 8+1) = \text{Beta}(18,\ 9)。

经过第二次更新,你对 pp 的期望值从 9/17≈0.5299/17 \approx 0.529 变为 18/27=2/3≈0.66718/27 = 2/3 \approx 0.667。由于第二次观测的命中率(0.90.9)非常高,它将你的信念又重新向上拉高。此时 α+β\alpha+\beta 的总和达到了 2727,信念曲线比之前更加尖瘦,表示基于更多的数据,你对命中率 pp 的估计变得更加确定。值得注意的是,最终的估计 0.6670.667 虽然高于第一次更新后的 0.5290.529,但并没有接近第二次观测的 0.90.9,这是因为第一次的“不佳”表现仍然被保留在信念中,体现了贝叶斯更新“累积所有历史数据”的特点。篮球投篮命中率信念更新的三个阶段如图5所示,虚线标出了各个阶段的期望命中率。


原文配图
图5 篮球投篮命中率信念更新的三个阶段



5. 众数与期望:哪个“典型值”更典型?

前面我们已经反复用到了贝塔分布的期望公式 E[p]=α/(α+β)E[p] = \alpha/(\alpha+\beta)——它就是伪成功数占总伪实验数的比例,非常直观。 聊完了期望,你可能心里会冒出一个很自然的问题:期望这个数,它自己出现的可能性大吗?或者说,整个分布里,哪个 pp 值出现的可能性是最大的?

这就引出了另一个“典型值”——众数(mode),也就是概率密度函数那个最高峰对应的 pp 值。对于贝塔分布,当 α>1\alpha > 1 且 β>1\beta > 1 时,它的众数有个很简单的公式如下。 最可能出现的 p=α−1α+β−2\text{最可能出现的 } p = \frac{\alpha - 1}{\alpha + \beta - 2}

把它和期望公式并肩放着看。 期望(平均)=αα+β,众数(最可能)=α−1α+β−2\text{期望(平均)} = \frac{\alpha}{\alpha + \beta}, \quad \text{众数(最可能)} = \frac{\alpha - 1}{\alpha + \beta - 2}

眼熟吗?形式很像,但众数公式的分子分母都各少了一点东西。这小小的差别,其实藏着直观的道理。期望(Mean)是“平均脸”,它照顾了所有可能性的重心。众数(Mode)是“最强音”,它只关心密度最高的位置。这个众数公式适用于 α>1,β>1\alpha>1,\beta>1,来自对 Beta 密度函数求导,而不是简单地把先验里某部分“去掉”。

还记得那个 Beta(5,2)\text{Beta}(5, 2) 吗?我们算算看。

  • 期望:5/(5+2)≈0.7145/(5+2) \approx 0.714。
  • 众数:(5−1)/(5+2−2)=4/5=0.8(5-1)/(5+2-2) = 4/5 = 0.8。

期望说“平均来看 pp 大概是 0.7140.714”,但众数说“pp 最有可能的值是 0.80.8”。哪个对?都对了。它俩描述的是同一个分布的不同侧面。

Beta(5,2)\text{Beta}(5,2) 的分布如图6所示,期望(虚线)与众数(点线)不重合。你可以这么想象:这个分布是一个有点向右歪的小山丘。山的“重心”(期望)在 0.7140.714,但山的“最高峰”(众数)在更右边的 0.80.8。为什么峰顶会更靠右呢?因为小山丘的右侧拖了一条细长的“尾巴”,把重心往左边拉了一点。期望是概率加权平均的位置,众数是概率密度最高的位置。


原文配图
图6 Beta(5,2) 的分布



为什么公式里要“减1”? 从密度函数看,Beta(α,β)\mathrm{Beta}(\alpha,\beta) 的密度正比于 pα−1(1−p)β−1p^{\alpha-1}(1-p)^{\beta-1}。寻找峰值时,对这个函数求导并令导数为0,就会得到 (α−1)/(α+β−2)( \alpha-1)/(\alpha+\beta-2)。因此,“减1”来自密度函数的幂次结构。伪计数直觉可以帮助记忆,但不要把它理解成真的删掉了一次成功和一次失败。

那到底该看期望还是看众数? 这取决于你的问题。

  • 如果你在做长期预测或决策,关心的是大量重复下的平均表现,那么期望是你的朋友。它稳健、直观,就是伪成功数占总数的比例。
  • 如果你想知道“下一次,哪个 pp 值最容易被抽到?”或者想找到分布最密集、最核心的位置,那么众数更合适。它在视觉上就是那个最高点。

好消息是,当 α\alpha 和 β\beta 都很大时(例如你有了很多很多数据),减去的那点 11 和 22 就无关紧要了,期望和众数会几乎重合。那时,分布会变成一个又高又瘦的尖峰,你用一个数就能很好地概括它。

几个有趣的特例

  • α=β=1\alpha = \beta = 1:均匀分布。每个 pp 的可能性都一样,没有唯一的“最高峰”。
  • α<1,β<1\alpha < 1, \beta < 1:U形分布。最高峰出现在两个极端 p=0p=0 和 p=1p=1 处。
  • α=2,β=3\alpha = 2, \beta = 3:期望是 0.40.4,众数是 1/3≈0.3331/3 \approx 0.333。这次众数跑到了期望的左边。

期望告诉你重心在哪儿,众数告诉你最高点在哪儿。两个数一起看,对分布的把握就更立体。

6. 更深一层的联系:顺序统计量的视角

贝塔分布还有一个非常“实在”的出身,这能帮助我们更牢固地把握它。还记得最简单的均匀分布吗?假设我们从 Uniform(0,1)\text{Uniform}(0,1) 中随机独立抽取了 nn 个样本,然后把它们从小到大排好序:X(1)<X(2)<…<X(n)X_{(1)} < X_{(2)} < \dots < X_{(n)}。这个排序后的数列叫“顺序统计量”。

那么,排在第 kk 位的那个数 X(k)X_{(k)},它自身的分布是什么?答案是:X(k)∼Beta(k,n−k+1)X_{(k)} \sim \text{Beta}(k,\ n-k+1)。

为什么?直观上想:要第 kk 个顺序统计量落在 pp 附近,我们需要在它左边有 k−1k-1 个点,在它右边有 n−kn-k 个点。一个点落在 pp 左边(即落在 [0,p][0,p] 内)的概率恰好是 pp,落在右边的概率是 1−p1-p。这就很像一个多项分布。严格推导需要一点组合数学,但结论极其简洁。

不仅如此,我们还可以从期望的角度获得更直观的理解。前面我们学习过,贝塔分布 Beta(α,β)\text{Beta}(\alpha,\beta) 的期望是 α/(α+β)\alpha/(\alpha+\beta)。因此,X(k)∼Beta(k,n−k+1)X_{(k)} \sim \text{Beta}(k, n-k+1) 的期望就是 k/(n+1)k/(n+1)。这有一个直观的几何解释:当我们从均匀分布中随机抽取 nn 个点并将它们排序后,这些点将区间 [0,1][0,1] 分割成了 n+1n+1 个子区间。由于均匀分布的对称性,这些子区间的平均长度应该是相等的,都等于 1/(n+1)1/(n+1)。那么第 kk 个顺序统计量的期望位置,就应该是从 00 开始数,平均经过了 kk 个这样的子区间,即 k/(n+1)k/(n+1)。例如,我们在均匀分布的章节中计算过,最小值 X(1)X_{(1)} 的期望是 1/(n+1)1/(n+1),最大值 X(n)X_{(n)} 的期望是 n/(n+1)n/(n+1)。直观和严格推导的结果吻合,也从另一个角度说明了贝塔分布与均匀分布之间的内在联系。

7. Python实验室

下面是书里配套的 beta_bayesian_coin_toss.py,复制即可运行。

"""
文件名: beta_bayesian_coin_toss.py
功能: 模拟贝叶斯更新过程:从先验信念出发,通过观测硬币抛掷数据,逐步修正为后验信念。

【这是什么?】
这段代码演示了贝叶斯统计的核心思想:
“信念(后验) = 旧信念(先验) + 新证据(数据)”
你可以设定一个“上帝视角的真实概率”(比如硬币的真实偏差),然后看看需要多少次实验,
才能把你最初设定错误(或无知)的信念拉回到真相附近。

【怎么用?】
1. 修改下方【可调整参数配置区】的数值。
2. 运行脚本。
3. 观察生成的图像:
   - 虚线是你最初的猜想(先验)。
   - 蓝线是看过数据后的修正猜想(后验)。
   - 竖线是上帝视角的真理。
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# --- 可调整参数配置区 ---
# 1. 上帝视角:硬币真实的正面概率 (0到1之间)
# 试着把它改成 0.3 或 0.8,看看贝塔分布能不能找到它
TRUE_P = 0.65  

# 2. 实验数据:我们要抛多少次硬币?
# 试着对比 N=10 (数据少,信念动摇) 和 N=1000 (数据多,铁证如山) 的区别
N_TRIALS = 50  

# 3. 你的先入之见:初始的 Alpha 和 Beta
# Beta(1, 1) = 完全无知(平坦)
# Beta(10, 10) = 觉得应该是公平的,但不太确定
# Beta(100, 1) = 顽固地认为一定是正面(偏见极重)
PRIOR_ALPHA = 2  
PRIOR_BETA = 2   

# ------------------------------------------------------------------

def run_bayesian_update():
    # 1. 设置画图风格
    # 使用简洁的样式,避免视觉干扰
    plt.style.use('bmh')
    fig, ax = plt.subplots(figsize=(10, 6))

    # 2. 生成模拟数据 (这是你在现实中观测到的结果)
    # np.random.binomial 模拟了 N 次伯努利试验
    # 结果是一个数组,其中 1 代表正面,0 代表反面
    data = np.random.binomial(n=1, p=TRUE_P, size=N_TRIALS)
    
    # 统计观测到的成功(正面)和失败(反面)次数
    observed_heads = np.sum(data)         # 成功的次数 k
    observed_tails = N_TRIALS - observed_heads # 失败的次数 n-k
    
    print(f"【实验报告】")
    print(f"真实概率 (上帝视角): {TRUE_P}")
    print(f"观测数据: 投掷 {N_TRIALS} 次,发现 {observed_heads} 次正面,{observed_tails} 次反面")

    # 3. 执行贝叶斯更新 (核心数学步骤)
    # 根据共轭性原理:后验参数 = 先验参数 + 观测计数
    # 这行代码对应数学公式:alpha_new = alpha + k
    posterior_alpha = PRIOR_ALPHA + observed_heads
    # 这行代码对应数学公式:beta_new = beta + (n - k)
    posterior_beta = PRIOR_BETA + observed_tails

    # 4. 准备绘图数据
    # 创建一个从 0 到 1 的 x 轴,用于绘制概率密度曲线
    x = np.linspace(0, 1, 1000)
    
    # 计算 先验分布 的密度值 (pdf)
    y_prior = stats.beta.pdf(x, PRIOR_ALPHA, PRIOR_BETA)
    
    # 计算 后验分布 的密度值 (pdf)
    y_posterior = stats.beta.pdf(x, posterior_alpha, posterior_beta)

    # 5. 绘制图形
    # 绘制上帝视角的真理线
    ax.axvline(TRUE_P, color='black', linestyle='-', alpha=0.5, label='Truth (Unknown to you)')
    
    # 绘制先验信念 (虚线)
    ax.plot(x, y_prior, 'g--', lw=2, alpha=0.6, label=f'Prior: Beta({PRIOR_ALPHA}, {PRIOR_BETA})')
    ax.fill_between(x, y_prior, color='green', alpha=0.1)

    # 绘制后验信念 (实线 - 你的最终结论)
    ax.plot(x, y_posterior, 'b-', lw=3, label=f'Posterior: Beta({posterior_alpha}, {posterior_beta})')
    ax.fill_between(x, y_posterior, color='blue', alpha=0.2)

    # 添加图表信息
    ax.set_title(f'Bayesian Update: Belief Change after {N_TRIALS} Coin Flips', fontsize=14)
    ax.set_xlabel('Probability of Heads ($p$)', fontsize=12)
    ax.set_ylabel('Density (Belief Strength)', fontsize=12)
    ax.legend(loc='upper left')
    ax.set_xlim(0, 1)
    
    # 去掉不必要的边框
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)

    print(f"【更新结果】")
    print(f"先验信念: Beta({PRIOR_ALPHA}, {PRIOR_BETA})")
    print(f"后验信念: Beta({posterior_alpha}, {posterior_beta})")
    
    # 计算并打印后验期望 (Mean = alpha / (alpha + beta))
    posterior_mean = posterior_alpha / (posterior_alpha + posterior_beta)
    print(f"你的最终猜测 (后验期望): {posterior_mean:.4f} (距离真相差 {abs(posterior_mean - TRUE_P):.4f})")

    plt.tight_layout()
    plt.show()

if __name__ == "__main__":
    run_bayesian_update()

试着把先验参数设为 Beta(100, 1)(哪怕还没扔你就坚信这一定是枚正面硬币),然后把真实概率 TRUE_P 设为 0.5(公平硬币)。运行代码,看看现实世界的数据是如何把你那条偏得离谱的信念曲线,一步步强行“修正”回真相附近的。

8. 总结

现在你再看贝塔分布,它还是那一串有点复杂的公式吗?我希望你看到的是以下几方面。

  1. 信念描述工具:通过 α\alpha 和 β\beta,你能刻画从“完全无知”到“极度确信”,从“对称”到“极端”的各种关于概率 pp 的先验认知。
  2. 知识更新系统:它与二项分布的共轭性,提供了一套“伪计数”的增量学习算法,让贝叶斯更新变得像做加法一样简单。
  3. 桥梁:它的一端连着最简单的均匀分布(作为特例),另一端通过共轭性连着离散的二项分布;它的归一化常数连着伽马函数;它本身还描述了均匀分布样本的次序特性。

在学本节之前,你可能认为“概率”就是一个像 0.50.5 这样的固定数字。现在你知道了,当我们谈论一个未知事件的概率时,更诚实的说法是:我们用一个分布(例如贝塔分布)来描述这个概率的可能性。 这个分布的期望值给出了我们的最佳点估计,而这个分布的宽度(方差),则诚实地反映了我们对这个估计究竟有多大的把握。

真正的智慧,不仅在于知道某个答案,更在于清晰地了解自己对这份答案的不确定程度。 贝塔分布让你从“我认为概率是 0.60.6”,进步到“我认为概率最可能是 0.60.6,但它在 0.50.5 到 0.70.7 之间的可能性是 90%90\%”。

AI视野 · 推荐算法的“探索”本能

你是否好奇过,为什么今日头条或抖音总能精准拿捏你的喜好,偶尔还会推一些冷门内容给你“尝鲜”?这背后的核心策略之一就是汤普森采样(Thompson Sampling),而它的发动机正是贝塔分布。

在推荐系统中,AI 把每个内容的点击率(Click-Through Rate,CTR)建模为一个贝塔分布。当它不确定你喜不喜欢一个新视频时,分布会很宽(α,β\alpha,\beta都很小),AI 就有较大概率随机抽到一个高分并推荐给你(这是“探索”);一旦你滑走了(β+1\beta+1),分布变窄且左移,AI 就知道下次少推这类东西(这是“利用”)。


2026年10月29日更新:我的书终于出版了。这半年以来,有不少朋友在催更这本书,非常感谢大家的耐心等待,我在文章末尾放了商品链接,有兴趣的朋友可以买来阅读~

这是一本适合自学的概率论与数理统计教材,700多页,用大白话、生活中的例子和详细推导,把公式背后的道理讲清楚,也把各个知识点之间的联系串起来。

书里还结合了不少AI相关内容,包括softmax、余弦相似度、交叉熵和KL散度等,并配有“AI视野”和“Python实验室”,介绍这些数学知识在AI中的应用,用代码模拟和验证。备考方面,收录了2024—2026年考研数学一、数学三的部分概率论真题,以及大厂算法岗面试题的详细解答。

正在学概率论、准备考研,或者学AI想补数学基础的朋友,欢迎阅读。

知乎原文

作为一个算法工程师,开发算法的痛点是什么?

做了六年算法工程师,我先后待过几家公司。结合我自己和身边同事朋友的经验,说几个我觉得这行特有的痛点。

其他很多回答已经把技术层面说得挺好了:数据脏、算力不够、线上线下指标对不上、部署难、badcase修不完。都经历过,不重复了。说几个我觉得更磨人的。


一、训了三天,没有成果,周会上说什么?

这应该是算法岗和开发岗最根本的区别。

后端开发一个功能,写完了就是写完了,bug修好了就是修好了。算法有点不一样,训了三天模型,指标纹丝不动,甚至变差了。这就是常态。换个学习率试试,换个数据增强策略试试,换个loss函数试试。结果每一次“试试”都是一两天起步。

但周会还是要照开。leader问你这周做了什么,你总不能说“我试了五组超参数,都不work”吧?事实就是这样。于是像我这种老油条学会了一套表述方式:把“没训出来”说成“在探索中”,把“复现不了论文的结果”说成“正在排查环境差异”,把“完全没有思路”说成“在做前期调研”。

就算实验有了结果,你也说不清“好”到底是怎么来的。是模型架构选对了?是数据刚好比较干净?还是某个不起眼的trick碰巧起了作用?做搜索推荐的同行应该有感触——离线AUC对不上线上AUC,线上AUC对不上AB指标,好几百行代码跑完一轮实验,效果很差,归因也归因不出来,那一周的产出就是零。开发同事看到的是“这个功能上线了没有”,我们看到的是这个指标涨了0.3个点但不确定是不是噪声。

搞算法就像炼丹:你不知道它为什么行,也不知道它为什么不行,而你的绩效取决于它行不行。

之前在知乎上刷到过一句话:“送外卖的被困在算法里,搞算法的被困在AB实验里。”


二、我的title是“VLM预训练方向”

在顺丰的时候,我的方向是把30多个子业务各自训练的54个小模型统一到一个视觉大模型基座上。听起来很前沿对吧——VLM预训练。

但我到手的第一件事是什么呢?

顺丰的不同业务线对同一个东西有不同的叫法。这边叫“快递”,那边叫“包裹”,第三个叫“快件”。标签体系完全不统一。我需要手动写映射规则,把170个目标标签一个个对齐。

而且这事不是我自己闷头就能干的。30多个子业务,我得一个一个去找对应的业务负责人对齐口径——先在内部沟通软件上问人家有没有空,约个时间,走到人家工位旁边,坐下来掏出表格,一条一条过:“你们这个‘大件包裹’和那个组的‘超规快件’是一个东西吗?”有的人很配合,有的人忙得根本约不上。30多个业务,一轮都没对完,上游的标签又改了,前面的工作部分作废。

写映射规则的过程中我还发现了149个“幽灵标签”——存在于系统里,但没有对应任何一条实际数据。还有大约26万条数据记录,挂着标签但内容是空的。不把这些清理掉,训出来的模型就是在学噪声。

所以我的日常就是在Excel和数据库之间反复横跳,对齐口径、清理脏数据、写映射规则。一个292条的映射规则表,是我那段时间的主要产出。

这不是顺丰的问题,每家公司都一样。在OPPO做多模态的时候也是,做数据清洗的时间远远多于做算法创新的时间。我在牛客上看到过一个话题叫“dirty work”,134K浏览,底下的人描述的日常跟我一模一样:标注、清洗、对齐、跑数、催数。

上面好几个回答都提到了数据问题,但我觉得比数据本身更磨人的是那个落差——你告诉别人你是做大模型预训练的,别人觉得你好厉害。你心里知道你每天在干什么。


三、真正做算法的时间,可能只有10%

一天8个小时(实际远不止),算法工程师有多少时间在写代码、调模型?

早上来了先站会,然后是跨部门的需求对齐会,然后产品那边拉你评审一个新需求,下午有个技术方案评审,再加一个业务复盘会。中间穿插着被人在群里@问“这个case为什么推荐结果不对”。等终于坐下来准备跑个实验,已经下午四五点了。

我有一次快下班了被临时拉进一个会,我第一反应是想装作没看见消息,但最后还是硬着头皮去了。

有个同行总结得特别到位:“80%都因为层层汇报或者是信息差带来的无用功,剩下的20%里,有至少一半都是在做最终产出的数据美化和口径对齐,最多剩下那么10%是真正影响线上收益或者用户体验的工作。”

10%。你真正在“做算法”的时间,可能就这么多。剩下的都在开会、对齐、汇报、写文档、回复消息、处理工单。

而且算法岗的会有一种独特的难受:很多时候业务方在讨论他们自己的需求,你作为算法侧被拉进来“听一下”,全程既插不上话也帮不上忙,就是坐在那里。等到最后他们说“这个我们后面跟算法同步一下”,你才知道自己为什么在这儿——他们需要你的工时,不一定需要你这个人。

有人说“对齐是最隐形的消耗”,放在算法岗特别准。跟产品对齐需求、跟数据对齐口径、跟工程对齐接口、跟业务对齐目标。每一次对齐本身不长,加起来就把一天吃掉了。


还有很多痛点没展开的,随手列几个:

绩效归因困难:模型效果取决于数据质量、产品方向、流量分配、评测口径一堆变量,但绩效系统最终归因到你个人头上。你控制不了结果,却要为结果负责。

技术迭代的焦虑:去年刚学会的框架今年就过时了,每隔几个月出一个新范式,你永远在追,永远觉得自己落后。有同行说“我的学习速度不可能赶上AI的,多年经验和语言优势都被抹平了”。

身体上的消耗:久坐、熬夜、长期高压,颈椎腰椎眼睛胃,互联网体检报告没几个是干净的。

还有就是:“你们拿的钱多呀”。所有算法工程师试图表达这些痛点的时候,都会被这句话堵回去。

这些我后面可能会单独写。我之前写过两本技术书(概率论已经出版了,大模型技术书即将出版),现在在写第三本,关于算法工程师的职业痛点。就是想把这些真实的、具体的、日常的东西记录下来。

如果你也有特别想说的,或者经历过一些特别具体的瞬间,欢迎私信我聊聊——说不定会成为这本书里的一部分。

知乎原文

如何让大语言模型输出JSON格式?

这个问题前两年比较困扰我。那个时候经常调用大模型来生成训练数据,经常出现模型前面多一句“好的,以下是JSON 结果”,或者后面少一个引号,程序解析不了。现在已经有一些非常漂亮的解决方法了。最优雅的方法之一是,很多大模型厂商,比如 DeepSeek 的 API,自己有专门的 JSON 模式,可以让模型按 JSON 格式输出。

但是合法 JSON 只能保证格式对了,不代表字段也对。我把几个最主流的方法都分享一下吧。

下面是我从《一个大模型算法工程师的诞生》书稿里抽出来的一个样章小节。这本书我已经交稿,将会在不久之后由清华大学出版社出版。


你以后天天要用的东西

这一节的内容,可能看起来不像Transformer、不像注意力机制那样高大上。它不涉及什么精妙的数学推导,也不会出现在顶会论文的标题里。但我可以非常负责任地告诉你:在你真正坐到工位上干活的头三个月里,这一节讲的东西,你用到的频率可能比前面学的所有理论知识加起来还高。

为什么这么说?因为在真实的项目里,大模型的输出几乎从来不是给人直接看的。它的输出是要接到一条流水线里——被程序解析、被数据库存储、被下游系统消费。而程序不会理解自然语言。程序需要的是严格的、可预期的、结构化的数据格式。JSON是目前最通用的那种格式。

你会在各种各样的场景里碰到这个需求。做智能客服的时候,用户发来一条消息,模型要从里面提取意图和关键信息,输出一个JSON,传给后台系统自动处理。用大模型批量生成训练数据的时候,每一条数据都必须是严格的JSONL格式,差一个逗号都喂不进训练脚本。用大模型做数据清洗和标注的时候,模型输出的分类标签、置信度、标注理由必须是结构化的,才能批量写进数据库供后续分析。后面如果接触到Agent(智能体)的开发,模型需要判断“该调用哪个工具、传什么参数”,这个判断结果也是一个JSON。

而且不仅仅是JSON。XML、YAML、CSV、自定义的模板格式,凡是结构化的输出,面临的问题和解决思路都是万变不离其宗的。这一节用JSON做主角,但你学到的方法论可以平移到任何结构化格式上。

我之所以纠结了很久最终决定把这一节加上,就是因为这个话题太实战了。它不会出现在任何教科书的目录里,但你第一天上班就会碰到,而且你碰到之后会发现,网上搜到的零碎经验帖根本帮不了你建立系统性的认知。我希望你读完这一节之后,面对结构化输出的问题,脑子里有一张清晰的地图,知道有哪些方案、每个方案保证了什么、没保证什么、什么时候该用哪个。

先看一个场景

想象你在一家做智能客服的公司,产品上线一周了,老板让你加一个新功能:用户发来一条退货请求,模型自动从里面提取关键信息,也就是商品名称、订单号、退货原因,然后输出一个JSON,传给后台系统生成退货工单。

你觉得这不难。模型理解能力挺强的,提取几个字段嘛,写个好点的prompt就行了。你花了半小时调好prompt,模型的回答看起来很完美:

商品名称:蓝色运动外套
订单号:20250415123
退货原因:尺码偏大

你把这个结果拿给后端同事看,他问你一个问题:“这个输出我怎么解析?”

当然,真要硬来,也可以按冒号和换行写规则去拆。但这种规则很脆:字段顺序一变、模型多说一句话、用户原因里自己带了换行,解析就可能出错。工程里更稳的做法,是让模型输出一种大家都认的固定格式,比如JSON。它长这样:

{"product": "蓝色运动外套", "order_id": "20250415123", "reason": "尺码偏大"}

程序拿到这个JSON之后,能精确地知道 product 对应什么、order_id 对应什么,然后把它们分别填进数据库的不同字段里。整个过程是自动的,中间没有人。

但如果模型的输出不是这个格式,哪怕只是少了一个引号、多了一个逗号、或者在JSON前面多说了一句废话,后端的Python代码在做 json.loads() 的时候就会直接报错。报了错,退货工单就生成不了。用户等半天没回音,投诉就来了。

好,你意识到了问题的严肃性。那就改prompt,明确要求模型输出JSON格式。这是所有人的第一反应,也确实是实战中的起点。

在prompt里说清楚

你在prompt里写得很详细:告诉模型“你只能输出一个JSON对象,不要输出任何其他文字”,列出每个字段的名称和类型,还给了一个完整的JSON示例。一个典型的prompt可能长这样:

你是一个信息提取助手。用户会发来一段退货请求,你需要从中提取关键信息,以JSON格式输出。

输出格式要求:
- 只输出一个JSON对象,不要输出任何其他文字
- JSON必须包含以下字段:
  product: 商品名称(字符串)
  order_id: 订单号(字符串)
  reason: 退货原因(字符串)

输出示例:
{"product": "黑色羽绒服", "order_id": "20250101456", "reason": "拉链损坏"}

你拿这个prompt跑了几条测试数据,模型表现得很乖,每次都老老实实地输出一个漂亮的JSON。你觉得问题解决了。

然后你自信满满地跑了500条真实的用户退货消息。翻车了。

翻车的六种姿势

打开日志一条一条看,发现翻车的姿势五花八门。

翻车一:模型忍不住说废话

这是出现频率最高的一种。你明明说了“不要输出任何其他文字”,但模型有时候就是忍不住:

好的,以下是提取结果:
{"product": "蓝色运动外套", "order_id": "20250415123", "reason": "尺码偏大"}

或者在JSON后面加一句:

{"product": "蓝色运动外套", "order_id": "20250415123", "reason": "尺码偏大"}
希望对你有帮助!如果还有其他问题请随时告诉我。

不管是前面多了一句话还是后面多了一句话,你的Python代码拿整段文本去做 json.loads(),都会直接报错。JSON解析器期望的输入是一个纯粹的JSON字符串,前面那句“好的,以下是提取结果:”在它眼里是非法字符。

为什么模型会这样?因为它在指令微调阶段被大量教导“要礼貌、要有帮助、要解释自己在做什么”。这些行为在对话场景里是优点,但在结构化输出场景里就成了捣乱:模型的礼貌本能和你的“只输出JSON”指令在互相拉扯,偶尔礼貌会赢。

还有一种更隐蔽的废话:模型用Markdown的代码栅栏把JSON包了起来:

``` json
{"product": "蓝色运动外套", "order_id": "20250415123", "reason": "尺码偏大"}
```

肉眼看上去挺规整的,但那三个反引号加上 json 这个标记,对JSON解析器来说都是非法字符。如果你没有针对这种情况做预处理,一样会报错。那简单啊,解析前先把代码栅栏剥掉不就行了?确实,很多项目最后都会写这类预处理。但这种翻车真正阴险的地方不在这里:模型版本更新之后可能突然开始偏好用代码栅栏包裹输出,你在旧版本上测的好好的prompt,换了新版本突然一大片报错,你去查日志才发现罪魁祸首是三个反引号。

锁定模型版本,否则你的线上系统会被“静默升级”搞崩

你用某家API跑通了结构化输出,上了线,稳定运行了一个月。某天早上你打开监控,发现格式报错率突然从0.5%飙到了8%。你的代码没改过,prompt没动过,数据也没变。查了两个小时才发现:API提供商在后台更新了模型版本,新版本的输出风格变了,比如开始偏好用Markdown代码栅栏包裹JSON。在调用API时,永远指定具体的模型版本号(比如用 gpt-4o-2024-08-06 而不是 gpt-4o),不要用会自动升级的别名。模型更新是好事,但应该由你主动测试后再切换,而不是被动地被“惊喜”。

翻车二:字段名乱跑

你要求的字段叫 product,但模型不一定每次都用这个名字。有时候它会这样:

第一条输出:{"product": "蓝色运动外套", ...}

第二条输出:{"product_name": "红色连衣裙", ...}

第三条输出:{"商品名称": "黑色皮鞋", ...}

三种写法,每种单独看都说得通。但你的下游程序只认 product 这一个名字,代码里写的是 data["product"],模型输出 product_name 的时候,程序找不到 product 这个key,直接抛 KeyError。

这种问题在输入内容比较复杂的时候尤其容易出现。模型看到用户的原文里写了“商品名称”四个字,就好心地把字段名也改成了中文,完全没理会你在prompt里定义的英文字段名。

翻车三:值的类型漂移

你期望价格字段是一个数字 99.5,模型给你输出了一个字符串 "99.5元",还带单位。或者你期望一个布尔值 true,模型输出了 "是"。

期望的:{"product": "蓝色外套", "price": 99.5, "in_stock": true}
实际的:{"product": "蓝色外套", "price": "99.5元", "in_stock": "是"}

你的下游代码拿到 price 之后要做数值计算,"99.5元" 这个字符串参与加减乘除会直接报类型错误。如果某个字段只能从几个固定值里选,比如 positive、negative、neutral,解析之后还要检查它是不是这三个值之一,不在名单里的就算格式对了也不能放行。

翻车四:JSON语法错误

这一类翻车肉眼很难看出来。你盯着模型输出看了半天觉得没问题啊,但解析器就是报错。典型的:

末尾多了一个逗号:

{"product": "蓝色外套", "order_id": "20250415123", "reason": "尺码偏大",}

最后一个字段后面那个逗号,在JavaScript里是合法的(叫trailing comma),但在JSON标准里是非法的。如果你写过JavaScript,你可能觉得这完全没问题,但JSON解析器不这么想。

用了单引号:

{'product': '蓝色外套', 'order_id': '20250415123'}

Python的字典用单引号没问题,但JSON标准要求必须用双引号。模型在训练数据里见过大量Python代码,偶尔会把Python字典的写法带到JSON输出里来。

还有一种更折磨人的情况,是JSON里面套JSON。比如你在做数据生成,让模型造一条SFT训练样本,外层格式是 {"instruction": "...", "output": "..."},而 output 的值恰好又是一段JSON字符串。这个时候,内层JSON的双引号必须转义成 \",否则外层解析器会在内层双引号那里提前断句。模型很容易在这里翻车:有时漏转义,有时多转义,有时把 \" 写成 \\"。遇到这种需求,别硬让模型一次性写对所有转义。更稳的做法是分两步:先让模型输出内层内容,再由你的代码把它塞进外层JSON,并负责转义。转义这种机械活,交给代码比交给模型可靠得多。

翻车五:截断

这种特别坑,因为报错信息不直观。模型输出的内容太长,撞上了你设置的 max_tokens 上限,被从中间硬生生切断了:

{"product": "蓝色运动外套", "order_id": "20250415123", "reason": "尺码偏

后面的引号、花括号全没了。你拿到的是一个残缺的JSON片段,根本无法解析。

翻车六:幻觉字段

你只要求三个字段,模型自作主张加了第四个:

{"product": "蓝色运动外套", "order_id": "20250415123", "reason": "尺码偏大", "confidence": 0.95}

confidence 是哪来的?你没要求这个字段,模型自己觉得加一个置信度挺合理的,就给你加上了。

另一种失败方向也要单独记住:模型可能觉得某个字段在这条数据里“不适用”,干脆跳过不输出。你的代码去取这个字段的时候,同样会遇到 KeyError;前一种是多了不该有的字段,后一种是缺了应该有的字段。

你可能会问:这些翻车是因为模型太笨吗?

不是。哪怕你用目前最强的模型,把prompt打磨到极致,只要跑的量够大,这些问题都会出现。原因在于模型生成文本的根本工作方式:它在生成每一个token的时候,你的prompt只是它参考的众多上下文信号之一。prompt说“只输出JSON”,这个信号确实能把模型推向正确的方向,但它不能保证每一步、每一次都占据绝对主导。一旦某一步生成偏了,比如多输出了一个“好”字,后续的生成就会顺着这个偏差继续走。模型不会在生成过程中突然“醒过来”说“哎不对我刚才不该说那个字”。错误会被放大,而不是被纠正。

本质上,prompt约束是一种软约束:它提高了正确格式出现的概率,但无法提供确定性的保证。

那怎么办?再写得更长一点、更凶一点,prompt里强调三遍“只输出JSON”?你已经试过了,真的不行。更稳的方向,是尽量把格式约束前移到生成过程里:不是在模型生成完之后去检查、去修补,而是在它生成每一步的时候,就不让它走错路。

从源头堵死:受限解码

前面所有的方案都是同一个思路:让模型自由发挥,出了问题再去擦屁股。现在要讲的方案彻底换了一个方向:在模型生成的过程中,从每一步就限制它只能走合法的路。

回忆一下前面讲解码策略时说过的流程:模型在每一步,会为词表里的所有token打分(这些分数就是logits),然后经过Softmax变成概率分布,最后从这个概率分布中采样出一个token。

受限解码插手的位置,就在Softmax之前。这里一定要分清楚:模型只负责给所有候选token打分;判断哪些token合法的,是外面的解码器、API或推理框架。它手里拿着一套JSON语法规则,会根据已经生成的前缀判断当前处在什么状态:是在等key,等冒号,等value,还是正在字符串中间。状态一确定,下一步哪些token合法也就确定了。

所以,受限解码做的事情是:在Softmax之前,把那些一选就会破坏JSON语法的 token 的分数强制设成负无穷大。

负无穷经过Softmax之后变成0。那些不合法的token被彻底从候选名单上移除了,模型不可能选到它们。

假设模型已经生成了 {"age": 这几个token,现在要决定下一个token是什么。简化一下,假设词表里只有5个候选token:

注意一个关键细节:hello 的原始分数最高,是4.0。模型最“想”输出的就是它。但 {"age": hello 不是合法的JSON。JSON的值要么是数字,要么是用双引号包裹的字符串,要么是 true/false/null,裸单词 hello 不在合法选项里。} 不合法的原因不同:age 的值还没给,对象不能就这么结束。, 也一样,值还没给,不能跳到下一个字段。

如果不做任何干预,直接对这5个分数做Softmax:

\(e^{3.0}\approx20.1\),\(e^{2.5}\approx12.2\),\(e^{4.0}\approx54.6\),\(e^{1.0}\approx2.7\),\(e^{0.5}\approx1.6\)

总和约91.2。

算出的概率分布:2 占22%," 占13.4%,hello 占59.9%,} 占3%,, 占1.8%。

hello 有将近60%的概率被选中。一旦选了它,JSON就坏了。

加上受限解码之后,先把不合法的token的分数设为 \(-\infty\):

修改后的分数:\([3.0, 2.5, -\infty, -\infty, -\infty]\)

\(e^{-\infty} = 0\),所以Softmax的计算只剩两个有效的数字:\(e^{3.0} + e^{2.5} \approx 20.1 + 12.2 = 32.3\)。

新的概率分布:2 占62.2%," 占37.8%,其余三个都是0%。

模型现在只能在输出一个数字和开始一个字符串之间选择。不管选哪个,JSON的语法都不会被破坏。

原文配图

上图直观地展示了这个变化。左右两张图的对比,就是受限解码做的全部事情。

这就是受限解码做的全部事情:把不合法的选项从菜单上划掉。模型原本想输出什么没有变(原始logit不变),只是犯错的自由被拿走了。就像把一道填空题变成了选择题:填空题里你什么都能填,填错了就是语法错误;选择题里每个选项都是合法的,你随便选都不会出格。

在计算机科学里,这个负责记住当前语法状态的东西叫做“状态机”。JSON的语法规则是固定的,所以这个状态机可以提前写好,在模型生成每一步时查一下就行,额外的计算开销很小。

但有一个至关重要的点必须说清楚:受限解码只保证语法正确,不保证内容正确。

在刚才的例子里,模型被限制在 2 和 " 之间选了 ",然后继续生成了 "二十五岁"。最终输出是 {"age": "二十五岁"},语法完全合法,json.loads() 解析毫无问题。但如果你的下游系统期望 age 是一个整数25而不是字符串二十五岁,这个值就是错的。受限解码管得了括号有没有闭合,管不了模型填的内容对不对。

这个区分非常重要,后面还会专门讲。先继续往下走。

受限解码会不会降低生成质量

常见问法:“你说受限解码把不合法token的概率设为零,那这样不就干预了模型本来的概率分布吗?会不会导致模型输出的内容变差?”

典型错误:要么一口咬定“不会有任何影响”,要么夸大影响说会严重降低质量。

正确思路:影响确实存在,但在结构化提取场景下通常可以忽略。关键要分两种情况。第一种,模型正在输出JSON的“骨架”部分(花括号、冒号、引号、字段名),这些位置合法的token本来就很少,受限解码基本不会改变模型的选择。第二种,模型正在输出字段的“值”部分(比如在 "name": " 后面填内容),这时候大部分词表token都是合法的(因为它们都可以作为字符串的一部分),受限解码几乎不做任何干预。真正可能出问题的是极端情况:Schema非常复杂,模型被迫生成一个它“不想”生成的嵌套结构,这时它在结构骨架上被迫选了低概率token,可能影响后续内容的连贯性。但对于典型的信息提取任务(字段不多、嵌套不深),这个影响微乎其微。

API帮你做好了

受限解码是底层原理。你可能在想:我需要自己写一个JSON状态机吗?

好消息是,如果你用的是主流API,不需要。API提供商已经把这件事封装好了,暴露成简单的参数让你一键开启。目前主流的封装分成两个层级,很多人容易搞混,这里要仔细区分。

第一个层级:JSON Mode

你在调用API的时候,加一个参数告诉它“我要JSON格式的输出”。JSON Mode 会约束输出的语法,比如不在 JSON 前面多说一句话,也不在最后一个字段后面多放一个逗号。

但注意:它只保证“是合法的JSON”,不保证“是你想要的那种JSON”。

什么意思?你的prompt要求提取商品名称、订单号和退货原因,你开了JSON Mode,模型返回了这个:

{"result": "用户想退一件蓝色运动外套,订单号20250415123,原因是尺码偏大"}

这是合法的JSON吗?完全合法。一个对象,一个字段 result,值是一个字符串。json.loads() 解析毫无问题。

但这是你想要的结构吗?完全不是。你要的是 product、order_id、reason 三个独立字段,模型把所有信息糊成了一句话塞进了一个字段里。你的下游程序去取 data["product"] 的时候,KeyError,字段不存在。

很多人第一次用JSON Mode的时候就在这里栽跟头:“不是说保证JSON了吗?为什么我的程序还是报错?”程序报的是“找不到product这个字段”,并不是JSON解析错误。JSON Mode保证了语法,没保证结构。

第二个层级:JSON Schema模式

这个层级比JSON Mode更进一步。你不仅告诉API“我要JSON”,还传入一份完整的JSON Schema。

JSON Schema是什么?它本身也是一段JSON,只不过它描述的是“数据应该长什么样”的规则:哪些字段必须有、每个字段的名字叫什么、值是什么类型(字符串、数字、布尔值还是数组)、嵌套结构是什么样的。你可以把它理解成一份验收标准:模型的输出必须完全符合这份标准,否则API不会放行。

API拿到你传入的Schema之后,会在底层构建一个状态机,和前面讲的受限解码思路一致。在模型生成的每一步,它只让模型选那些既符合JSON语法、又符合你Schema定义的token。只要你在Schema里把必填字段、类型和额外字段规则写清楚,模型就不能少字段、不能乱加字段、不能搞错类型。

这才是真正意义上的想要什么格式就得到什么格式。

两个层级的区别用一句话概括:JSON Mode保证“这是一个合法的JSON”,JSON Schema模式保证“这是一个符合你定义的那种合法JSON”。 前者管语法,后者管语法加结构。

不同API提供商对这两个层级的命名和参数写法各不相同,变化也快,查文档就能找到,不值得死记。重要的是理解这两个层级各自保证了什么、没保证什么,这个认知不会过时。

Function Calling:模型不只是输出JSON,还帮你决定该做什么

到目前为止,所有方案的目标都是一样的:让模型输出一个格式正确的JSON。但想一想:你要这个JSON干什么?在实际工程中,拿到JSON之后的用途一般是:查数据库、调用某个API、在后台系统里创建一条记录、发一封邮件。

function calling(也叫tool use,工具调用)把这个意图直接摆到了台面上。用退货客服的例子来走一遍。

先从你的代码开始。你在代码里定义一组“工具”。每个工具有三样东西:名字、描述、参数的JSON Schema。比如你定义了一个工具叫 create_return_order,描述是“创建退货工单”,参数Schema要求有 product(字符串)、order_id(字符串)、reason(字符串)。

你可能还定义了另一个工具叫 query_order_status,描述是“查询订单状态”,参数只需要一个 order_id。

你在调用API的时候,把这些工具定义和用户的消息一起发给模型。

这一步很关键:模型之所以知道有哪些工具可用,是因为你在这次请求里把工具清单也交给了它。

接着,用户说:“我三天前买的蓝色外套想退货,订单号20250415123,太大了。”模型看了用户的消息和你定义的工具列表,判断出“这个用户想退货,我应该调用 create_return_order 这个工具”。

然后,模型不会直接给用户写一段自然语言回复,而是输出一个结构化的工具调用请求。内容大致长这样:

{
  "tool": "create_return_order",
  "arguments": {
    "product": "蓝色外套",
    "order_id": "20250415123",
    "reason": "尺码偏大"
  }
}

如果接口开了严格模式,arguments 就要符合你给的参数 Schema。

到这里,模型的任务就结束了一半。真正执行动作的是你的代码。你的程序解析出工具名 create_return_order 和参数。检查参数没问题后,再调用你写好的退货工单创建函数,把参数传进去。函数在后台系统里创建了一条退货记录,返回了结果:“退货工单已创建,工单号RT20250419001”。

有些场景还会多走一步:你把函数的执行结果再发回给模型。模型看到了“退货工单已创建”的结果,再基于这个结果生成一句自然语言回复给用户:“您的退货申请已提交,工单号是RT20250419001,预计3个工作日内处理。”

原文配图

上图清晰地展示了这条边界:模型的箭头只到你的代码就停了,从不直接通向任何外部系统。模型本身没有调用任何函数。 function calling 这个名字容易让人误以为模型自己去执行了什么操作。实际上模型做的事情只有一件:输出一段JSON,表达“我认为应该调用这个函数,参数是这些”。真正的函数调用发生在你的代码里。模型负责决策,程序负责执行。这个区分在后面接触Agent开发的时候会变得非常重要。

从结构化输出的角度看,function calling其实就是两件事的组合:结构化输出 + 意图路由。 参数 Schema 规定了参数应该长什么样,意图路由让模型判断应该调用哪个工具(从你定义的工具列表里选)。里面没有一个全新的解码原理。扒开皮看,你可以把它理解成:结构化输出那套格式约束,加上工具选择这一步。

function calling的价值不在于发明了什么新技术,而在于它把模型输出结构化数据和数据驱动下游操作天然地绑在了一起。你不需要先让模型输出JSON,再自己写代码解析JSON,再根据解析结果决定调哪个函数,function calling把中间的胶水层省掉了。如果你的场景是模型做决策、程序做执行,function calling几乎就是首选方案。

不过,function calling还有一个细节很容易被新人漏掉:模型不一定每次都会调用工具。如果用户问的是“今天天气怎么样”,而你定义的工具只有退货和查单,模型可能判断没有合适的工具可用,直接返回普通文本。这意味着你的代码不能默认返回结果里一定有 tool_calls,而是要同时处理两种情况:一种是工具调用请求,另一种是普通文本回复。很多线上小事故就出在这里:代码直接去取 tool_calls,没做空值判断,一遇到普通回复就报 NoneType 错误。

有些API还允许你强制模型必须调用某个工具,这个功能要谨慎用。强制调用意味着即使用户的问题和工具无关,模型也会硬凑一个调用出来,参数很可能是瞎编的。除非业务上真的要求“每次请求都必须落到某个工具”,否则一般让模型自己判断更稳。

最后一道安全网:解析、校验、重试

到这里,你已经看到了几种更硬的方案:受限解码、JSON Mode、JSON Schema、function calling。它们能把格式成功率拉得很高,但线上系统里仍然要留一层安全网。原因很简单:输出可能被截断,API可能返回空内容,模型可能拒答,网络可能超时,字段的值也可能不符合业务约束。

这层安全网不用写得很玄,核心就是四步:先解析,再校验,失败就重试,最后记录日志。解析负责判断这段文本能不能作为合法JSON读取;校验负责判断字段齐不齐、类型对不对、枚举值有没有越界;重试负责把具体错误告诉模型,让它重新输出;日志负责把救不回来的样本留下来,别让一条坏数据拖垮整个批次。

以前那种“找第一个左花括号和最后一个右花括号”的方法,只适合低成本原型。它能救一点模型多说废话的情况,但遇到多个JSON对象、字符串里有花括号、输出被截断,就很容易误切。真正上线时,更可靠的做法是优先用API提供的JSON Schema或function calling;不支持这些能力时,再把“提取、解析、校验、重试”作为兜底流程。

校验最好交给代码来做。Python里可以用Pydantic这类工具定义字段和类型:缺字段、类型错、枚举值不在范围里,直接报出具体位置。转义、尾逗号、字段类型这些机械活,也尽量交给代码,不要让模型硬背规则。

重试也要有边界。一次失败后,把错误信息带回去让模型重来,通常很有效;连续两三次仍然失败,就不要继续烧钱了。记录原始输入、模型输出、报错信息和使用的prompt版本,后面才能分析到底是输入太脏、Schema太复杂、模型太弱,还是你的格式要求写得不清楚。

拿到一个“要JSON”的需求,先问三个问题

你在工位上会反复遇到一类需求,表现形式各不相同。有时候是“帮我从这批文本里提取关键字段”,有时候是“模型的回答要能被下游系统解析”,有时候是“我要用模型批量生成训练数据”,但本质上都是同一件事:让模型的输出变成程序可以直接消费的结构化数据。

前面把prompt约束、受限解码、JSON Mode、JSON Schema和function calling都拆开讲了一遍。真正坐到工位上接需求的时候,你不需要把这些方案重新背一遍。每次拿到这种需求,先问三个问题,答案会把你直接导向最合适的方案。

第一个问题:你用的是什么模型,怎么部署的?

如果你用的是主流闭源API(比如通过API调用大模型),先去翻一下文档,看它支持到哪一层:JSON Mode、JSON Schema模式,还是function calling。只支持JSON Mode,能约束 JSON 语法;支持JSON Schema模式,结构就能被卡住;支持function calling,模型还能顺手帮你做工具选择。先把平台能给你的硬约束摸清楚,再往下判断需求。

如果你用的是本地部署的开源模型,看一下你的推理框架(比如 vLLM)有没有guided decoding(也就是前面讲的受限解码)功能。有的话,效果和闭源API的JSON Schema模式基本等价。没有的话,你的起点只能是“prompt约束 + 后处理”。别灰心,很多线上项目就是这么跑的。

第二个问题:你的输出结构是固定的,还是动态变化的?

如果每条数据的输出结构都一样,也就是同样的字段名、同样的类型、同样的嵌套,这是最适合JSON Schema模式的场景。定义一次Schema,所有数据复用。

如果你的API只支持JSON Mode,没有JSON Schema模式,也不是不能用。它至少能帮你挡掉尾逗号、少括号、多废话这些语法问题。只是字段缺失、字段名漂移、类型不对这些结构问题,仍然要靠后处理校验来兜底。

如果输出结构会根据输入变化,比如不同类型的商品需要提取不同的字段,你就需要在代码里维护多套Schema定义和对应的解析逻辑,工程复杂度会上升。这种情况下,prompt约束 + 后处理反而更灵活。你可以在prompt里根据输入类型动态拼接不同的格式要求和示例。

第三个问题:你的需求是“得到一个JSON”,还是“让模型驱动下游操作”?

如果你只是需要把模型的输出解析成结构化数据存起来、做分析、或者喂给另一个模型,优先考虑JSON Schema模式。如果平台只支持JSON Mode,就用JSON Mode配后处理校验;再退一步,才是prompt约束 + 后处理。

如果你需要模型根据用户的请求,判断应该执行什么操作、传什么参数,比如用户说“帮我查一下订单状态”,模型要判断“应该调用查询订单的函数,参数是订单号”,这是function calling的典型场景。它比单纯的结构化输出多了一层“意图路由”的能力。

三个问题回答完,你的方案基本就定了。但不管选了哪个方案,有一件事永远要做:在后处理中加一层校验。 哪怕你用了JSON Schema模式、理论上格式不可能出错,也在代码里加一个 try-except 包住 json.loads(),加一个Pydantic模型做字段校验。这么做的理由很朴素:线上系统需要对所有异常情况都有兜底,网络超时、API返回格式变化、模型版本静默更新,这些事随时可能发生。防御性编程的成本极低,但它能在关键时刻救你一命。

还有一条贯穿所有方案的原则:先试成本最低的,不行再升级。 如果你的场景是每天跑几十条、容忍偶尔出错,prompt约束就够了。如果prompt约束的成功率不够、或者你的量级上来了,先看平台有没有JSON Mode;JSON Mode够用就先用它,再配后处理校验。还不够,或者你需要确定性的字段结构,再升到JSON Schema模式。需要让模型决定调用哪个工具,再用function calling。不要一上来就用最重的方案。工程中的好决策,关键是用刚好够用的工具。

实战中几个隐蔽的坑

最后聊几个教科书里很少写、但实际项目里很容易碰到的坑。

  • 结构化输出本身也会吃token。

字段名、冒号、逗号、双引号、示例里的完整JSON骨架,都会被算进输入或输出成本里。字段很多、调用量很大时,字段名尽量简短,极少出现的字段可以考虑设为可选,不要让模型每次都输出一堆 null。

  • 温度参数和结构化输出的关系。

前面讲解码策略的时候说过,温度越高,模型输出越随机、越有创造力。但在结构化输出的场景里,创造力恰恰是你不想要的。你希望模型老老实实地按格式输出,不要搞创新。所以做结构化提取的时候,通常把temperature设得很低,接近0。如果你用了受限解码或JSON Schema模式,温度的影响会小很多(因为不合法的token已经被屏蔽了),但如果你只靠prompt约束,温度拧高是在给自己找麻烦。

  • Schema太复杂,模型会分心。

当你的JSON结构有三四层嵌套、十几个字段、每个字段还有各种条件约束的时候,你会发现一个奇怪的现象:格式倒是对了,但字段里填的内容质量明显下降。模型并没有变笨,它只是把太多注意力分给了“搞清楚该输出什么结构”,留给认真理解输入内容的注意力就少了。实战中的应对策略是:把一个复杂的提取任务拆成多次调用,每次只提取一部分信息,每次的Schema保持简单。

  • 截断是最常被忽视的隐患。

前面在翻车案例里提过,这里再强调一次防御方式:根据你的Schema估算最长可能的输出长度,给 max_tokens 留出足够余量。在后处理中加一个简单的检查:先看 API 为什么停下来。比如返回 finish_reason=length,就先别用这份结果,给输出多留点长度再试。正常结束也照样解析和校验。

  • 小模型的格式遵从能力弱得多。

如果你只用过大的闭源模型,可能对这个问题完全没有感知。但当你尝试在7B甚至更小的开源模型上用prompt约束来获取JSON输出时,你会发现成功率断崖式下降。同样的prompt,在大模型上90%的成功率,在小模型上可能只有50%。这通常不是prompt写得差,问题更多出在小模型对指令的遵从能力本身就弱。它的参数量不够支撑它同时理解输入内容和遵守格式约束。在本地部署小模型的场景里,受限解码经常是上线门槛。

  • 流式输出和结构化输出有冲突。

很多API默认是流式返回的。模型生成一个token就发一个token回来,让用户看到“正在打字”的效果。但流式返回意味着你在收到完整输出之前没法做JSON解析。如果你的场景需要结构化输出,要么关掉流式、等模型全部生成完再一次性拿到结果去解析;要么在流式接收的过程中先把token拼起来,前端可以展示“正在生成”的状态,让用户知道系统没卡住,但JSON解析要等全部收完之后再做。这不是什么大坑,但第一次碰到的时候容易困惑。

  • 字段名用中文还是英文。

一句话的建议:字段名永远用英文,值里面该中文就中文。字段名是给程序读的,英文不会有编码问题,也和绝大多数编程语言的命名习惯一致。值是给业务用的,中文数据就用中文,不需要翻译。

把两个问题分清楚

这一节讲了很多方案和技巧,但在结束之前,有一个贯穿始终的认知值得单独拎出来强调,它能帮你在面对结构化输出问题时少走弯路:

格式正确和内容正确是两个完全独立的问题。

受限解码、JSON Schema、function calling,这些方案解决的全部是格式问题。它们保证输出的JSON语法合法、结构符合预期。但JSON里填的内容对不对,提取的名称是不是原文里的、数字是不是准确的、分类是不是正确的,这些,它们一概管不了。

打个比方:这些方案保证的是信封的形状和尺寸完全符合邮局的规格要求。但信封里装的那封信写了什么、写得对不对,和信封的形状没有半点关系。

内容质量取决于模型本身的能力、prompt的质量、以及输入数据的清晰程度。这些是前面prompt engineering讲的东西,以及后面微调相关章节要解决的问题。

把这两件事分清楚,你在排查问题的时候就不会走弯路。下游系统报错了,先判断是格式问题还是内容问题。格式问题用这一节讲的方案解决。内容问题回去优化prompt、改进数据、或者换一个更强的模型。两类问题的解法完全不同,混在一起只会越修越乱。


这部分内容改写自我正在写的《一个大模型算法工程师的诞生》里的对应小节。书里的版本会放更多上下文、图示和练习。

知乎原文

如何评价Gilbert Strang最新的《线性代数》(第6版) 中文版?

我在大厂做大模型预训练,同时也在给清华大学出版社写一本概率论教材《大白话概率论》。所以我看Strang这本书,比较关心的是:这里面有多少东西是我工作中真正用得上的。

答案是:比我预期的多很多。

国内大部分线性代数教材在行列式和特征值那里就结束了,Strang这本书的第9章和第10章直接把线性代数拉进了优化和深度学习的战场。这两章是第6版最值得单独拿出来说的部分。

第9章:梯度下降与最优化

9.1节讲梯度下降,Strang不是像深度学习教材那样直接扔一个更新公式给你,而是先从"梯度向量的几何意义"讲起——梯度方向是等高线的法线方向,沿着梯度的反方向走下降最快,但不一定走得最远。然后他用一个"之字形"的例子,让你亲眼看到最速下降在狭长椭圆上的灾难性表现。我在实际训练大模型的时候,learning rate调不好就会看到loss曲线剧烈震荡,本质上就是这个之字形问题。Strang从线性代数的角度把这件事讲透了——问题出在Hessian矩阵的条件数上。这个理解比"试试看把lr调小一点"有用得多。

9.2节讲反向传播,这一节我觉得是全书对工程师最友好的部分。他把反向传播拆成了两件事:多变量链式法则提供数学骨架,计算图提供实现路径。很多深度学习课程讲反向传播的时候要么纯讲代码要么纯推公式,Strang的做法是先用矩阵语言把链式法则写清楚,再告诉你这个矩阵乘法在TensorFlow里长什么样。对于每天都在写训练代码但偶尔会对梯度计算感到心虚的工程师来说,这一节值得反复读。

第10章:线性代数视角下的深度学习

10.1节做了一件我在其他教材里几乎没见过的事:他把ReLU激活之后的神经网络理解为一个分段线性函数,然后用线性代数的语言去数"这个函数到底有多少个线性分块"。这个视角非常漂亮——它把"深度网络的表达能力为什么强"这个问题,变成了一个可以用矩阵维度和层数来定量回答的数学问题。

10.3节讲均值、方差和协方差矩阵。这一节和我正在写的概率论书有大量交集,但Strang的切入角度完全不同。我的书是从概率论出发,先讲随机变量再讲协方差;Strang是从数据矩阵出发,先讲矩阵的列向量之间的内积,然后告诉你这就是协方差。两条路到达同一个终点,但对不同背景的读者各有各的好处。如果你先读过概率论的协方差定义,再来看Strang用矩阵语言重新表述一遍,你会突然理解为什么PCA就是对协方差矩阵做特征值分解——因为在Strang的框架里,这两件事根本就是同一件事。

原文配图

有一点值得提前说明:第10章讲的深度学习内容主要集中在CNN、ReLU和基础的神经网络结构,还没有涉及Transformer和注意力机制这些近几年的架构。这很正常,Strang的定位始终是一本线性代数教材,不是深度学习教材,他做的事情是把线性代数的地基帮你打扎实。如果你想从这个地基出发去理解大模型,中间还需要自己再走一步。

原文配图

总结

如果你是一个已经在工业界写模型的人,第9章和第10章值得你认真读,它们能帮你把每天在用的工具从"会用"升级到"理解"。如果你是一个正在学线性代数的学生,这本书前8章的经典内容不用我多说,Strang讲线性代数的能力是公认的,第9和第10章则会让你提前看到这些数学将来会去往何方。

知乎原文

为什么函数求导之后表达式长度大大超出原来的表达式?

PyTorch的计算图机制没准可以回答这个问题。
首先,什么是表达式?比如 f(x)=x⋅sinxf(x) =x\cdot sinx ,这是一个表达式。求导之后变成 sinx+x⋅cosxsinx+x\cdot cosx ,表达式变长了。
但是,凭什么非要用“表达式”来表示函数?
用表达式表示函数只是一种选择,不是唯一的选择。

PyTorch 不存表达式,而是画了一个计算图。当你对这张图求导的时候,每个节点只需要知道自己这一步的局部导数,然后把数值往前传。

举个具体的例子,假设你有:
L=(x⋅w)2L = (x \cdot w)^2

x是输入数据,w是参数,L是损失函数。你想知道 ∂L∂w\frac{\partial L}{\partial w} ,也就是 L 对 w的导数,这样才能更新 w。

我们先手动算一遍:
∂L∂w=2(xw)⋅x\frac{\partial L}{\partial w} = 2(xw) \cdot x

代入具体数字,比如 x=2,w=3 x=2, w=3 :

∂L∂w=2×6×2=24\frac{\partial L}{\partial w}= 2 \times 6 \times 2 = 24


然后以具体的代码为例:

import torch

x = torch.tensor(2.0)
w = torch.tensor(3.0, requires_grad=True)

L = (x * w) ** 2
L.backward()

print(w.grad)  # 输出 24.0

它怎么算出 24 的?

你写 x * w 的时候,PyTorch 不只是算出数值 6,它同时在后台建了一张图,记录下:

  • 有一个乘法操作
  • 它的两个输入是 (x) 和 (w)
  • 输出是 6

你写 ** 2 的时候,它又记录:

  • 有一个平方操作
  • 输入是 6
  • 输出是 36

整张图长这样:

x=2    w=3
  \    /
   乘法  → 6
     \
     平方 → 36 = L

这张图就叫计算图,每个节点是一次操作,边是数据流向。

然后你调用backward(),这个函数的意思是:从 (L) 出发,往回走,把每个参数的梯度算出来。 这个往回走就是反向传播,本质是链式法则。

具体怎么走:

第一步:(L) 对自己的导数是 1。(废话)

第二步:往回走到平方操作。 平方操作是 L=u2L = u^2 ,其中 u=x⋅w=6u = x \cdot w = 6 。 ∂L∂u=2u=12\frac{\partial L}{\partial u} = 2u = 12

第三步:继续往回走到乘法操作。 乘法操作是 u=x⋅wu = x \cdot w 。 ∂u∂w=x=2\frac{\partial u}{\partial w} = x = 2

第四步:链式法则把两步串起来: ∂L∂w=∂L∂u⋅∂u∂w=12×2=24\frac{\partial L}{\partial w} = \frac{\partial L}{\partial u} \cdot \frac{\partial u}{\partial w} = 12 \times 2 = 24

每一步,PyTorch 只做了非常简单的事:

  • 平方操作的导数是 (2u),代入数值得 12
  • 乘法操作对 (w) 的导数是 (x),代入数值得 2
  • 乘起来得 24

从头到尾,PyTorch 连一个 L=(x⋅w)2L = (x \cdot w)^2 这样的式子都没写出来过,全程就是算数、算数、算数。顺着计算图走了一遍,每步算一个数。

神经网络是一个超级超级大的计算图。 可能有几亿个参数,几百层操作,但原理完全一样:前向传播建图,backward() 从后往前走一遍,每个参数得到一个梯度数值。 每一个操作(加法、乘法、ReLU、Softmax……)都提前写好了自己的导数规则,backward 走到哪个节点就调用哪个规则,算出数值继续往前传。 这次调用 backward(),要的是参数取当前这些值时的梯度,用不着先把导数写成一个长式子。

回到开头的 x sin x。先只对前面的 x 求导,后面照抄,得到 sin x;再反过来,得到 x cos x。原来只有一个乘积,现在得把两项加起来。式子再套几层,每一步都摊开写,同一段还可能被抄好几遍。

知乎原文

什么是「伯克森悖论」,这种现象在生活中有什么影响?

最大的影响是,我们曾认为是科学标准的控制变量法,很可能会产生错误的结果。

先问个问题:为什么“长得好看的演员,演技一般很烂”?又或者“长得帅的男生,大概率是个渣男”?

其实这些结论未必正确。在概率论里,有一个经典的统计学错觉——伯克森悖论。

在回答这个问题前,先复习一下常识。几百年的科学史告诉我们要用“控制变量法”来识破虚假的关系。 比如,统计数据发现:冰淇淋销量越高的月份,溺水死亡的人数就越多。难道吃冰淇淋会导致溺水?当然不是。因为它们背后有一个共同的原因(简称“共因”)——夏天天气热。只要我们“控制”住气温这个变量,冰淇淋和溺水的虚假关系瞬间就消失了。

控制“共因”,能帮我们找到真相,这是科学常识。 但是!如果你控制的不是“共因”,而是“共果”(共同的结果)呢?灾难就发生了。

原文配图

回到“帅哥出渣男”和“美女没演技”的问题。

假设在全人类这个大盘子里,颜值和人品是完全独立的两码事。长得好看的人,可能专一也可能渣;长得丑的人,同样可能专一也可能渣。它们之间没有任何因果关系。

但问题是,你能接触到的“约会对象”或者在屏幕上看到的“明星”,是所有人吗?显然不是,你在挑选约会对象时,一个长得又丑、性格又极其恶劣的人,在第一轮就被你拉黑了。你能留下来的观察样本,要么是长得帅的,要么是性格特别好(能提供情绪价值)的。

同理,大众之所以会关注到一个演员,通常是因为他要么长得极其好看,要么演技极其出众。至于那些长得又难看、演技又差的人,在海选阶段就被淘汰了,根本进不了你的视线。

也就是说:“进入你的视线”,就是颜值和演技导致的一个共同结果(共果)。

这就挖了个大坑:你观察的池子,是被“必须至少有一项特长”这个条件过滤过的。你实际上已经在不知不觉中“控制”了变量。

在这个池子里,如果你看到一个演员长得貌若天仙,你的潜意识会自动降低对他演技的预期;反过来,如果一个演员长相磕碜却依然能在圈里混,你会立刻断定:“这人绝对是老戏骨,演技炸裂!”

明明颜值和演技在总体中没关系,但在“进入大众视线”这个筛选条件下,它们呈现出了负相关。两个原本八竿子打不着的事情,只要你加了一个“筛选条件”,就会看起来像是此消彼长了。

原文配图
在所有点中,颜值与演技的相关系数几乎为 0;但被“门槛线”筛选出的红色区域,凭空出现了一条强烈的负相关趋势。

伯克森最早讨论的,就是只看医院里的病人会有什么问题。我们拿骨折和肺炎举个例子。假设在普通人群里,骨折和肺炎是相互独立的。再假设这家医院只收两种病人:骨折的,或者得了肺炎的,有其中一种就能住院。

那么,你在医院里看到一个没骨折的病人,他肯定得了肺炎,不然他怎么住进来的?可如果他已经骨折了,那他有没有肺炎,都能住进来。所以,只看住院病人,骨折的那一组,得肺炎的比例反而更低。难道骨折能预防肺炎?当然不是。是“住院”这个条件先替我们筛了一遍。

这种现象在生活中随处可见,随便举几个例子:

  • 自古红颜多薄命:丑且短命的普通人,根本不会被写进历史书或者野史八卦里被你看到。
  • 寒门出贵子:家境贫寒且一生平庸的人,是上不了新闻头条的。你能看到的寒门,必定是出了贵子的。
  • 学霸往往书呆子:在能考进顶尖名校的群体里,如果一个人智商没有那么逆天,他必定付出了极其可怕的死记硬背(显得像书呆子)。这让你产生了“努力/书呆子”和“高智商”成反比的错觉。

如果只是人类八卦一下也就罢了,但在今天,伯克森悖论很可能会给大模型(AI)开发带来偏见。

现在的深度学习大模型极其依赖海量的数据。模型本身没有常识,它只会从你给的数据里找规律。 比如大厂用的“AI 简历筛选系统”。如果你只拿“最终被录用的员工”数据去训练 AI。注意,“成功录用”就是一个被严重筛选过的池子(共果)。 在这个池子里,学历平平的人,可能是靠沟通和实战能力过关的;而学历极高的人,可能沟通能力只是一般。AI 在扫描了几十万份简历后,会敏锐地学到一个极其荒唐的逻辑:“高学历 = 沟通能力差”。 接下来,它就可能会淘汰那些既有高学历、沟通能力又强的人,因为它觉得这种人“不符合历史规律”。

再比如训练能看图说话的多模态AI。工程师为了保证数据不被污染,会写脚本剔除“画质差 且 描述烂”的数据。结果在留下的优质语料库里,AI 发现:图片越是一塌糊涂,附带的文本描述反而越极其专业(因为只有配文好,糊图才会被保留)。这就把 AI 忽悠瘸了,学到了“图片越模糊,文本越可靠”的荒谬底层逻辑。

几百年的科学史告诉我们要用“控制变量法”来排除干扰。但如果我们不小心把“共同的结果”给控制住了,原本没关系的两件事,看起来就可能有关系,把人和 AI 都带进沟里。

代码验证

为了让大家更直观地看到“筛选”是如何扭曲数据的,我用 Python 写了个最简单的模拟。代码放在最后,大家可以自己跑一下。 我们随机生成 10000 个毫无关系的“颜值”和“演技”得分。在全集中,这两者的相关系数接近于 0。 但是,当我们加一个 颜值 + 演技 > 某个及格线 的筛选条件,把模拟出道明星的数据单独拎出来算相关性时。原本毫无关系的两个人设,瞬间变成了 -0.45 左右的强负相关。

import numpy as np
import scipy.stats as stats

# 1. 随机生成10000个人的颜值和演技,两者毫无关系(独立)
np.random.seed(42)
looks = np.random.normal(50, 15, 10000)
acting = np.random.normal(50, 15, 10000)

print(f"总体相关系数: {stats.pearsonr(looks, acting)[0]:.3f}")
# 输出结果:总体相关系数: 0.003 (极其接近0,完全不相关)

# 2. 加上娱乐圈的筛选条件:颜值+演技必须大于110才能进入大众视野
is_famous = (looks + acting) > 110

# 3. 把出道明星的数据单独拎出来,再算一次相关性
famous_looks = looks[is_famous]
famous_acting = acting[is_famous]

print(f"明星池子里的相关系数: {stats.pearsonr(famous_looks, famous_acting)[0]:.3f}")
# 输出结果:明星池子里的相关系数: -0.457 (呈现出强烈的负相关!)

2026年10月30日更新:我的第一本书终于出版了!

伯克森悖论这个问题,我在书的第 3.5 节里也讲过。

这些年,我在知乎写了不少概率论和数学相关的回答。现在,我的第一本书《大白话概率论:从数学直觉到AI视野》终于由清华大学出版社出版了。这半年有不少朋友一直在等,非常感谢大家的耐心和支持~

这是一本适合自学的概率论与数理统计教材,700多页,延续了我写回答时的讲法:用大白话,讲得很详细,把公式背后的道理讲清楚,也把各个知识点之间的联系串起来。

书中还结合了交叉熵、KL散度等AI相关内容,并设有“AI视野”和“Python实验室”。此外,也收录了2024—2026年考研数学一、数学三的部分概率论真题,以及大厂算法岗面试概率题的详细解答。

如果你喜欢这篇回答的讲法,也想系统地学一遍概率论,可以看看这本书。 下方放了购书卡片,感谢大家支持~

知乎原文

如何求P(AB)-P(A)P(B)的取值范围?

想到一种直观解法:P(AB)−P(A)P(B)P(AB)-P(A)P(B)这个式子可以理解成“协方差”。

先引入一个叫“开关”的概念:

如果事件A发生了,开关 IAI_A 就等于1;

如果事件A没发生,开关 IAI_A 就等于0。

(这个开关的学术名称叫做“示性函数”,你不用死记硬背,重要的是它背后代表的【开关】的思想)


接下来算一下这个开关的期望(也就是平均值)。因为只有1和0两个状态,所以:

期望E(IA)=1⋅P(A)+0⋅[1−P(A)]=P(A)E(I_A)=1\cdot P(A)+0\cdot[1-P(A)]=P(A)。

同理,对于事件B的开关,E(IB)=P(B)E(I_B)=P(B)。


那么, IAI_A 乘以 IBI_B 代表什么呢?

看看 IAI_A乘以IBI_B会有哪几种情况:

情况1:A发生了,B也发生了。此时 IA=1I_A=1,IB=1I_B=1。乘积是 1×1=11\times1=1。

情况2:A发生了,B没发生。此时 IA=1I_A=1,IB=0I_B=0。乘积是 1×0=01\times0=0。

情况3:A没发生,B发生了。此时 IA=0I_A=0,IB=1I_B=1。乘积是 0×1=00\times1=0。

情况4:A没发生,B也没发生。此时 IA=0I_A=0,IB=0I_B=0。乘积是 0×0=00\times0=0。

只有在“A和B同时发生”这一种情况下,这个乘积才等于1。只要有任何一个没发生,乘积立马变成0。

所以,我们完全可以把“IAI_A乘以IBI_B”看作是一个新的总开关,这个总开关负责一件事:记录“事件AB”有没有发生。发生了它就是1,没发生它就是0。

所以IAI_A乘以IBI_B,其实就是事件AB(同时发生)的开关。

它的期望E(IAIB)=1⋅P(AB)+0⋅[1−P(AB)]=P(AB)E(I_AI_B)=1\cdot P(AB)+0\cdot[1-P(AB)]=P(AB)。


回顾一下,两个变量X和Y的协方差公式是:

Cov⁡(X,Y)=E(XY)−E(X)E(Y)\operatorname{Cov}(X,Y)=E(XY)-E(X)E(Y)

把我们的开关代进去:

Cov⁡(IA,IB)=E(IAIB)−E(IA)E(IB)=P(AB)−P(A)P(B).\begin{aligned}\operatorname{Cov}(I_A,I_B)&=E(I_AI_B)-E(I_A)E(I_B)\\&=P(AB)-P(A)P(B).\end{aligned}


P(AB)−P(A)P(B)P(AB)-P(A)P(B),就是事件A和事件B之间的协方差!它衡量的是这两个事件是互相吸引(正相关)还是互相排斥(负相关)。


既然知道了它是协方差,我们就可以用直观的经验来找它的最大值和最小值。


第一步:寻找最大值

协方差什么时候最大?当两个事件完全正相关的时候。

最极端的情况就是,事件A和事件B根本就是同一件事(比如A是抛硬币正面,B也是抛硬币正面)。


此时P(AB)=P(A)=P(B)P(AB)=P(A)=P(B)。

原式就变成了:

P(A)−[P(A)]2=P(A)[1−P(A)]P(A)-[P(A)]^2=P(A)[1-P(A)]。

对于x(1−x)x(1-x)的形式,我们用初中学的二次函数的知识x(1−x)≤(x+(1−x)2)2=14x(1-x) \leq(\frac{x+(1-x)}{2})^2=\frac{1}{4} ,可以知道它的最大值是14\frac14。

此时P(A)P(A)刚好等于12\frac12。


第二步:寻找最小值

协方差什么时候最小?当两个事件完全负相关的时候。

最极端的情况是,事件A和事件B互斥,永远不可能同时发生(比如A是抛硬币正面,B是抛硬币反面)。


拿刚才的硬币来说,正面和反面不可能同时出现,所以P(AB)=0P(AB)=0;而且总有一面朝上,所以P(A)+P(B)=1P(A)+P(B)=1。

原式就变成了:

0−P(A)P(B)=−P(A)P(B)=−P(A)[1−P(A)]0-P(A)P(B)=-P(A)P(B)=-P(A)[1-P(A)]。

为了让这个负数尽可能小,我们需要让P(A)[1−P(A)]P(A)[1-P(A)]尽可能大。

这又回到了第一步的情况,当P(A)=P(B)=12P(A)=P(B)=\frac12时,

P(A)[1−P(A)]P(A)[1-P(A)]达到最大值14\frac14,

对应地,−P(A)[1−P(A)]-P(A)[1-P(A)]达到最小值−14-\frac14。

那有没有可能比14\frac14还大?看P(A)P(A)和P(B)P(B)里较小的那个,叫它pp。两件事同时发生的概率不会超过pp,而两个概率相乘又至少是p2p^2。所以这一减,最多就是p−p2p-p^2,也就是刚刚算过的那个二次函数:

P(AB)−P(A)P(B)≤p−p2≤14P(AB)-P(A)P(B)\le p-p^2\le\frac14。

下界不用再算一遍。把BB换成“B不发生”,记作BcB^c。A发生、B不发生的概率就是P(A)−P(AB)P(A)-P(AB),而B不发生的概率是1−P(B)1-P(B)。代回去一算,整个差就刚好变号:

P(ABc)−P(A)P(Bc)=[P(A)−P(AB)]−P(A)[1−P(B)]=−[P(AB)−P(A)P(B)].\begin{aligned}P(AB^c)-P(A)P(B^c)&=[P(A)-P(AB)]-P(A)[1-P(B)]\\&=-[P(AB)-P(A)P(B)].\end{aligned}

既然正的最多到14\frac14,负的最少也就到−14-\frac14。


最后总结一下:

题目里的式子本质是A和B的协方差。

当A和B是同一个事件,且发生概率都是50%时,达到最大值14\frac14;

当A和B互斥,且发生概率都是50%时,达到最小值−14-\frac14。

所以它的取值范围就是[−14,14]\left[-\frac14,\frac14\right]。


以上内容节选自我的新书《大白话概率论讲义》。这本书主打用“大白话”把复杂的数学概念一步步讲透,每一节都配有 Python 实验代码辅助理解,并深度结合了大模型与 AI 的底层知识。目前全书已经定稿,正交由清华大学出版社进行校对,今年就会和大家见面。如果你想提前看一些样章,可以私信我加入新书内部试读群。


2026年10月30日更新:我的第一本书终于出版了!
这些年,我在知乎写了不少概率论和数学相关的回答。现在,我的第一本书《大白话概率论:从数学直觉到AI视野》终于由清华大学出版社出版了。这半年有不少朋友一直在等,非常感谢大家的耐心和支持~
这是一本适合自学的概率论与数理统计教材,700多页,延续了我写回答时的讲法:用大白话,讲得很详细,把公式背后的道理讲清楚,也把各个知识点之间的联系串起来。
书中还结合了交叉熵、KL散度等AI相关内容,并设有“AI视野”和“Python实验室”。此外,也收录了2024—2026年考研数学一、数学三的部分概率论真题,以及大厂算法岗面试概率题的详细解答。
如果你喜欢这篇回答的讲法,也想系统地学一遍概率论,可以看看这本书。 下方放了购书卡片,感谢大家支持~