第一次接触极大似然估计时,很多人都会有同样的困惑:公式里又是连乘又是取对数,最后还要求导,看起来像一道复杂的微积分应用题。但等你真正走进这个场景,会发现它本质上解决的是一个特别朴素的问题——我手上有一堆观测数据,我想反推这些数据最可能来自什么样的规律。
“极大似然估计”(Maximum Likelihood Estimation,简称 MLE)就是做这件事的标准框架。而 manim 这个数学动画引擎,最近在学习和教学圈子里被反复提起,是因为它特别适合把抽象推导变成“看得见的过程”。两个东西放在一起,刚好能组成一条从“知道公式”到“真正理解”的路径。
这篇文章我不打算只复述数学课本上的推导,而是想带你重新走一遍:从抛硬币问题出发理解似然的含义,再慢慢拆开为什么取对数、为什么求导、为什么有时候解不出来,最后讲清楚怎么用 manim 做辅助教学或自学动画。这是一套从概念到落地的方式,不是单纯讲公式。
1. 极大似然估计到底在做什么:它不是公式,是一次反推
1.1 从一个硬币实验开始理解“谁最有可能”
假设我拿了一枚不知道是否公平的硬币,连续抛了 10 次,结果是 8 次正面、2 次反面。你现在要猜这枚硬币正面朝上的概率 p 是多少。
直觉上,大多数人会说 p 大概等于 0.8。为什么?因为在独立重复实验里,观察到的频率是 8/10,那么真实概率就应该接近这个比例。但这里面其实暗含了一个很重要的判断逻辑:我们不是先知道概率再去预测结果,而是先有了结果,再反过去找“最能解释这个结果的概率”。这个过程,就是似然。
更具体地说,对于某个假设的 p,比如 p=0.5,计算出“抛 10 次出现 8 正 2 反”的概率是多少;再假设 p=0.8,同样计算这个概率是多少。哪个 p 让当前结果出现的概率更大,哪个 p 就更“像”真实概率。极大似然估计做的就是在所有可能的 p 取值里,找出那个让当前观测数据出现概率最大的值。
这个思维框架跟平时概率题不一样。概率题通常是“已知 p,算某件事发生的概率”,而极大似然估计是“已知观测结果,反推 p 是多少”。一个是正向的计算,一个是反向的推断。方向一变,整个问题的性质就不同了。
1.2 似然函数和概率函数:字母相同,含义不同
课本里经常会写 L(θ|x) 或者 L(θ),有时候还会和概率质量函数 P(x|θ) 混在一起。其实这两个函数写出来可能都是同一个式子,但看它的视角完全不同。
概率函数 P(x|θ) 是把参数 θ 当作固定已知,去描述随机变量 x 取不同值时的概率分布。比如 θ=0.5,那么 x 出现 4 次正面的概率是多少,这就是概率函数。
而似然函数 L(θ|x) 刚好反过来,它把观测到的 x 当作固定已知,然后去看不同 θ 取值下,这个固定 x 出现的可能性大小。这里用“可能性”而不是“概率”,是因为 θ 不是随机变量,它只是一个未知的固定参数,我们不能说 θ=0.8 的概率是多少,只能说在这个参数下观测数据出现的相对可能程度。
这个区别不是咬文嚼字。它决定了我们后面怎么使用这个函数。极大似然估计的目标就是找一个 θ,让 L(θ|x) 最大。因为 L 和 P 在数值上共享同一个形式,所以求极大似然估计,实质上是在参数空间里搜索一个点,让这个点对应的概率值最大。
明白了这一层,再看公式就不会觉得只是符号游戏。它是在用一个非常反直觉的方式处理不确定性:概率是给结果打分,似然是给参数打分。
2. 从零推导极大似然估计:为什么求导取对数会成立
2.1 写似然函数:把样本独立事件的概率乘起来
先从一个最简单的例子出发。假设我们有 n 个独立同分布的样本 x1, x2, ..., xn,都服从某个分布 f(x;θ),其中 θ 是未知参数。因为样本独立,所以同时观察到这一组样本的概率密度(或概率质量)就是每个样本概率的乘积,这就是似然函数:
L(θ) = ∏ f(xi;θ), i=1..n
如果是伯努利分布,比如抛硬币,正面概率是 p,那么一次观察出现正面就对应 f(1;p)=p,出现反面就是 f(0;p)=1-p。于是一组 8 次正面、2 次反面的观测,似然函数就是:
L(p) = p^8 · (1-p)^2
这个时候,你其实已经有了一个关于 p 的函数。你可以画一条曲线,横轴是 p 从 0 到 1,纵轴是 L(p)。你会发现曲线在某个点达到最高,这个最高点对应的 p 就是我们要找的极大似然估计。
这里有一个容易忽略的前提:我们为什么可以连乘?因为样本独立。如果样本之间不独立,连乘就不成立,后面的推导也全部作废。所以写似然函数之前,最好先确认“独立同分布”这个假设是不是合理。
2.2 取对数:不是为了“方便求导”这么简单
实际推导时,我们通常不会直接对 L(θ) 求导,而是先取对数得到对数似然函数:
ℓ(θ) = log L(θ) = ∑ log f(xi;θ)
很多教材只解释一句“为了把连乘变成连加,方便求导”。这个说法没错,但不完整。取对数真正重要的一点是:它保持了函数的凹凸性不变。因为对数函数是严格单调递增函数,所以能让 L(θ) 最大的 θ 值,也一定能让 log L(θ) 最大。也就是说,极大化对数似然和极大化原始似然是等价的。
那为什么要换成对数?原因有三个层面:
- 数学生成上,乘法的求导法则是加法求导法则不能比的。对数把连乘转成了连加,求导时就可以把每一项分别处理,公式复杂度显著下降。
- 数值上,连乘几十个 0.1 级别的概率会得到一个极小的数,比如 10^{-10} 甚至更小。计算机处理这种数容易出现浮点数下溢,在对数空间里这个值会变成 -10 甚至 -20,完全在安全范围。
- 优化上,很多数值优化算法是基于梯度做搜索。对数似然的梯度形式往往比原始似然更稳定,尤其在观测样本比较多的场景里。
不过要提醒一点:如果你只是手算推导,可能感觉不到取对数的好处,甚至觉得多此一举。但一旦到了用代码实现参数估计的时候,取对数基本是必须的。这是数学表达和工程实现的共同需求。
2.3 求导找极值:什么时候用闭式解,什么时候用数值优化
取完对数后,接下来的标准操作是求导,令导数为零,解出参数估计值。以我们的硬币例子为例:
ℓ(p) = 8 log p + 2 log(1-p)
对 p 求导并令其为零:
8/p - 2/(1-p) = 0
解得 p = 8/10 = 0.8。这刚好和直觉一致。这种能直接通过解析表达式求出的解,叫闭式解(closed-form solution)。
但很多实际分布做不到这一点。比如正态分布的两个参数 μ 和 σ²,虽然单参数情况下可以用偏导数解出来,但如果是更复杂的模型,比如逻辑回归中的参数,就没有闭式解,只能靠梯度下降、牛顿法等数值优化方法去逼近极大值。
所以学极大似然估计,不能只学“求导解方程”这一个套路,更要建立起一个判断:这个模型能不能直接解出来?如果解不出来,用什么样的优化方法?在工程中,很多所谓的“极大似然估计”其实是通过优化器迭代出来的近似解。
这里又引出一个边界:如果似然函数本身只有一个峰,那么数值优化很容易找到最大值;但如果有多个局部极大值,优化结果就依赖于初始值选择,甚至可能收敛到错误的地方。所以不能只看最终输出值,还要检查似然函数区域的整体形状。
3. 用 manim 把“似然函数变化”画出来:教学设计而非炫耀
3.1 为什么用 manim:它把静态公式变成动态推理
manim 是一个用于制作数学动画的 Python 引擎,最出名的是的数学解释视频。你不需要成为动画设计师,只需要用 Python 代码描述数学对象的移动、变换、出现和消失,manim 就会渲染成一段流畅的视频。最近想学 manim 的人越来越多,因为它确实能把很多“只可意会”的数学推导外化成视觉过程。
但我更想强调的是:manim 的价值不是做酷炫特效,而是帮助你或你的观众真正看见“变化”。在极大似然估计这个例子里,有非常多的动态关系:
- 随着 p 从 0 变化到 1,似然函数 L(p) 的曲线如何起伏;
- 当样本量 n 从 10 变成 100,曲线峰值位置是否稳定;
- 当观测结果从 8 正 2 反变成 50 正 50 反,估计值如何向 0.5 靠近;
- 取对数前后的函数形状有什么不同,极大点是否保持一致。
这些关系用文字写出来很容易,但看文字不会留下直觉。让曲线动起来,看它随着参数变化而变化,这个直觉就会慢慢长出来。
3.2 一个最小可用的动画脚本结构
manim 脚本有固定结构。以当前官方文档的写法为例,通常是定义一个继承自 Scene 的类,然后在 construct 方法里写画布上的对象。如果我做一个类似“不同 p 值对应的伯努利似然函数曲线”的动画,结构会大致长这样:
from manim import * class LikelihoodCurve(Scene): def construct(self): # 坐标轴 axes = Axes( x_range=[0, 1, 0.1], y_range=[0, 0.05, 0.01], x_length=6, y_length=4 ) # 先画一个函数曲线,后面再让它变化 graph = axes.plot(lambda p: p**8 * (1-p)**2) self.play(Create(axes), Create(graph)) self.wait()这只是一个示意,不是可以直接抄走跑的完整代码。因为我不能确定你本地的 manim 版本、Python 版本和渲染后端。但你可以从这种最小结构出发:先画静态曲线,确认坐标轴和函数没画错,再做动态参数改变。
如果你抓到了某段上游项目标题里的源代码材料,建议以实际代码为准。因为 manim 的 API 在不同版本里变化较大,比如 Lower 级别函数名、坐标轴参数写法、甚至类名都调整过。写代码前先锁定版本,能少踩很多坑。
3.3 从单参数到多参数:动画要突出什么
单参数 p 的动画相对简单,因为横轴就是参数,纵轴就是似然值。你可以在动画里同时展示样本点分布和似然曲线,让观众看到数据生成过程与参数搜索过程的对应关系。
多参数的情况要复杂很多,比如正态分布的 μ 和 σ。你没办法同时直观展示三维曲面和样本变化。这时我建议不要画完整三维曲面,而是采用“切片法”:固定 σ,画出关于 μ 的对数似然曲线;然后让 σ 取不同值,再画一组曲线叠加。观众会看到不同 σ 下曲线的峰值位置和尖锐程度在变化,这比一张三维图更容易理解。
另外一个很实用的动画策略是“对比展示”。把原始似然曲线和取对数后的曲线放在同一坐标系里,用不同颜色区分。虽然两者的纵轴量纲不一样,但你可以把两个函数各自做归一化,让观众看到它们的最高点在同一直线上。这个动画能非常直观地解决很多人的疑问:为什么 log 不影响极值点。
注意:manim 动画制作的常见问题不是“不够炫”,而是“重点太多”。一段动画里只讲一个关系,不要既画样本、又画曲线、又叠加积分区域。视觉过载会让理解变慢,而不是变快。
4. 真正要讲透极大似然估计,还需要补上这些边界
4.1 MLE 不是万能的:模型选择与过拟合
很多人学完极大似然估计,会觉得它是个“安全万能”的参数估计方法。但实际使用中,它的成立需要几个假设支撑:样本独立、分布假设正确、参数可识别、样本量不能太小。
先说分布假设。如果真实数据分布根本不是你所猜测的伯努利分布或正态分布,那么极大似然估计只能在你设定的模型类里找到一个“相对最好”的参数,它无法告诉你这个模型本身就错了。举个例子,如果数据其实是双峰的,你却用单峰正态分布去做估计,MLE 算出的 μ 可能落在两个峰之间,完全反映不了真实结构。这不是 MLE 的计算问题,而是模型设定问题。
再说样本量。虽然极大似然估计在很多情况下具有一致性——样本量越大,估计得越准——但小样本时它可能出现明显偏差。比如抛 2 次硬币都是正面,MLE 给出的 p 就直接是 1.0。这个结果很符合极大似然的逻辑,却完全不合常理。如果有先验知识,你可能更愿意用贝叶斯估计或加入正则化,而不是直接接受 p=1.0。
所以在实际项目中,MLE 只解决参数估计问题,它不解决模型选型问题。你需要自己保证分布假设、数据质量和样本代表性。这一点和机器学习里的过拟合很像:如果模型太灵活,MLE 会完全拟合噪声;如果模型太简单,又会有系统性偏差。
4.2 常见误区:不是每个 MLE 都有唯一解
极大似然估计存在几个现实中经常遇到的坑。
第一,无解。当似然函数在参数空间内没有最大值时,比如参数趋向边界导致似然函数单调上升,MLE 可能收敛到边界值而不是一个内部点。前面硬币例子里 p 的上界是 1,如果观测全是正面,MLE 就是 p=1,这本身合理但往往不是我们想要的估计。
第二,多解。有些模型的似然函数存在多个峰值,比如混合模型中不同成分参数交换,会给出同样的似然值。这时 MLE 的结果不代表唯一可识别,而是多个参数组合在似然意义上等价。要做可识别性分析,或通过约束让参数唯一化。
第三,数值不稳定。当参数数量很多,或者对数似然面很平坦时,优化算法可能走得很慢,或者停在某个平坦区域。这不是求导公式错了,而是数值优化的固有挑战。解决办法包括更精细的初始化、正规化、或者改用带约束的优化方法。
这些边界听起来有点打击人,但很必要。因为单学理论推导时,公式总是按要求成立,但真实数据从来没有严格按照假设生成过。学会 MLE,不只是会套公式,还要会判断什么时候可以用,什么时候它给出的结果要打问号。
4.3 从理论到落地:极大似然在真实项目中的位置
在真实系统里,极大似然估计不是一个独立步骤,而是概率模型建模链条中的一环。常见的落地流程是这样:
- 定义问题:你想预测的变量是什么?
- 选择概率分布模型:根据变量类型和生成过程,确定用伯努利、正态、泊松还是更复杂的模型。
- 写出对数似然函数。
- 用优化算法求参数极大值。
- 检查结果的合理性:参数是否在预期范围?似然值是否稳定?
- 做模型评估:用验证集或交叉验证看预测能力。
很多机器学习的算法本质就是在做极大似然估计。比如逻辑回归的损失函数是被设计成负对数似然的,最小化损失就是最大化对数似然。理解了 MLE,再去看那些损失函数,你会觉得它们的来历清楚很多。
但要注意,落地时还有一个工程点:数据量太大时,完整计算所有样本的对数似然会非常费时,于是出现了小批量随机梯度下降、增量估计等方法。它们本质上不是在计算精确的 MLE,而只是在近似它的方向。你写的日志里如果能区分“完整似然”和“近似似然”,排查问题时会更容易精准定位异常。
5. 从“看懂”到“讲透”:一套可复用的教学/学习框架
5.1 四步讲透法:场景、公式、动画、边界
如果你要用 manim 去讲极大似然估计,或者只是自己做一个学习总结,我推荐一套四步框架。这个框架同样适用于其他数学概念。
第一步,用真实场景建立直觉。不要一上来就甩定义。先给出一个硬币实验,或者一个“根据历史订单量估计需求增长率”的问题。让观众先能猜答案,比如猜 p 大概是 0.8。这样他们会主动想知道:怎么用数学证明这个直觉。
第二步,用公式把直觉形式化。从似然函数开始,逐步推到对数似然和求导。过程中不断强调:每一步的操作都没有改变“最大点”的位置,只是在换个更舒服的计算方式。这里适合用 manim 把曲线和公式同步显示,当函数曲线移动到最高点时,把“估计值”标注出来。
第三步,用动画把时间维加进来。把参数变化引起的曲线变化展示出来,比如样本量从 10 变成 1000 时,峰值位置越来越稳定。这是静态图无法传达的信息,也是 manim 最有价值的地方。
第四步,用反例划定边界。告诉观众 MLE 什么时候会失灵,比如小样本、模型错误、多峰似然。可以把一个错误模型下的估计结果画出来,让他们看到 MLE 也会给出很离谱的答案。
这套框架可以广泛应用于生成模型、衡量指标、优化目标等主题。核心原则只有一个:先给直觉,再给公式,再给动态过程,最后给边界。
5.2 制作 manim 动画时最容易踩的坑
我见过很多人刚开始学 manim 时,第一个项目就想直接复刻复杂视频,结果被渲染时间、版本兼容和代码结构劝退。这里有几个非常实际的提醒。
- 不要一上来追求 3D 和复杂变换。极大似然估计这种主题更适合 2D 坐标轴动画,3D 曲面反而会增加视角转换的认知负担。
- 确认你用的是哪个 manim 发行版。社区版(community edition)和原来的 3b1b 版 API 差别很大,网上很多教程代码不能直接照抄。最稳妥的办法是先跑官方示例,确认环境正常,再改代码。
- 动画帧率不要设置过高。常用的 30 FPS 或 60 FPS 都行,但高帧率会让渲染时间成倍增加。对讲解类视频,30 FPS 已经完全足够。
- 文字和函数同时出现时,注意留白。不要让公式覆盖曲线,也不要在同一画面塞太多信息。manim 的优势是让对象按顺序出现,而不是全部同时堆积。
如果你在写代码时遇到函数形状不对,先检查坐标轴范围。很多初学的错误不是函数本身错了,而是 x 轴和 y 轴刻度范围设置不当,导致曲线被拉伸得看起来很怪。把范围先设成和参数取值范围一致,曲线形状通常就正常了。
5.3 排查思路:动画不报错但效果不对怎么办
这一步专门给那些已经开始写 manim 脚本、但渲染出来的视觉效果和预期不符的开发者。排查顺序比直接找代码问题更重要。
第一,先区分是数学错误还是动画错误。你可以在动画代码之外,单独用 matplotlib 或直接在终端打印函数在几个关键点的取值,确认这个函数本身的形状。如果函数是对的,再去看 manim 的坐标变换。
第二,检查坐标轴与函数的空间映射。manim 的坐标轴默认有自己的渲染坐标,你在 axes.plot() 里传的是数学函数,但实际显示结果依赖坐标轴范围。如果函数峰值在 p=0.8,但画面里曲线的最高点不在 0.8 附近,大概率是 x_range 设置不对或者数据点采样太稀疏。
第三,检查 Play 语句的逻辑顺序。manim 是按时间播放一系列动画动作的,如果你希望“曲线随 p 变化”而不是“一条静态曲线出现”,你需要用 ParametricFunction 或者不断更新函数对象,而不是一次 plot。不同版本更新方式不同,建议先查文档再动手。
第四,看渲染日志和输出帧。manim 渲染时会产生很多日志,有时警告信息里藏着原因。比如采样点不足、对象超出画面范围,都会有提示。切忌只看最终 mp4 就问“为什么不灵动”,先看日志定位具体是哪一帧出问题。
排查时先缩小问题范围:先画一条静态直线,再画一条静态曲线,再让它动起来。每步都确认正常后再合并功能,能节省大量调试时间。
6. 写在最后:从这条曲线里,学到的不只是参数
回过头来看,极大似然估计这条曲线——似然函数之峰——其实刻画了一种非常底层的思维方式:所有模型都是我们对世界的猜测,数据则是裁判。我们不是在“找到真相”,而是在数据给定的条件下,挑选一个最不坏的解释。
manim 的价值则在于把思考过程显形。当你看到曲线随样本量收敛、随参数变化起伏时,你会有一种确凿的直观,而这种直观会在你写代码、调模型、看损失曲线时反复回响。
所以如果你是学生,先别急着背推导过程,试着用这个框架理解一次。如果你是开发者,试着做一个最小的 manim 动画,把自己最近理解的一个模型参数可视化。你是可以看到的,那个“由数据倒推规律”的过程,其实不太不抽象。