算法的故事 / 第一季第二季目录0 / 30 章已读

第二季 · 第 55 章

生命的家谱

《算法的故事》第二季 · 第五篇:破解生命的密码——从达尔文的第一棵树、分子进化,到Felsenstein最大似然与Saitou–Nei邻接法1837年,英国伦敦。一位刚刚结束环球航行的年轻博物学家,正在笔记本上思考一个问题。为什么不同岛屿上的生物,看起来既相似又不同?为什么某些已经灭绝的动物,与今天仍然活着的动物拥有相似的身体结构?物种之间的关系,究竟应该怎样表示?他在纸上画了一些向不同方向伸展的线条。一条线分成两条。两条又继续分叉。有些分支延伸得很远。另一些则在中途停止。在这幅草图上方,他写下了两个英文单词:I think.我想。这位年轻的研究者是:查尔斯·达尔文(Charles Darwin)。这张草图保存于达尔文1837年的物种演变笔记本B中。今天,人们可以在剑桥大学图书馆的档案中看到它。它并不是用DNA计算出来的进化树。也不是经过统计检验的物种关系图。而是达尔文在形成物种演化思想过程中,留下的一项珍贵记录。 [Darwin Correspondence Project][University of Cambridge]1859年,《物种起源》出版。达尔文使用分支树的结构,表达不同生物类群可能来自共同祖先,并在漫长时间中不断分化的思想。这种树,不是植物学意义上的真实树木。它是一种表示历史关系的数学与科学图形。每一条末端分支,可能代表一种今天仍然存在的生物。也可能代表一种已经灭绝的生物。每一个内部的分叉,表示某种共同祖先关系。但是,树上哪些分支应该连接在一起,不能只凭想象来决定。科学家需要证据。在达尔文的时代,主要证据来自:形态。解剖结构。化石。地理分布。以及不同生物的发育特点。一百多年以后,一种全新的证据进入了研究者的视野。DNA序列。科学家终于可以比较两种动物对应基因的核苷酸顺序。不仅能说:这两种动物的骨骼很相似。还可以进一步研究:它们在某个同源基因中,究竟有多少个位置不同?这些差异可能经历了怎样的演化过程?不同物种之间的距离,能否帮助我们恢复共同祖先的分支关系?但一个难题随之出现。如果有四种生物A、B、C、D。它们究竟应该组成:
text
((A,B),(C,D))
还是:
text
((A,C),(B,D))
又或者:
text
((A,D),(B,C))
这些符号表示不同的分组关系。但究竟哪一种更符合实际进化历史?如果增加到十种、二十种,甚至上百种生物,可能的树形数量将迅速增长。因此,重建生命的家谱,不仅是一个生物学问题。它同时是一项复杂的组合优化与统计推断任务。到了1981年,美国西雅图,一位遗传学研究者将最大似然思想系统地应用于DNA进化树推断。他叫作:约瑟夫·费尔森斯坦(Joseph Felsenstein)。1981年,他发表:Evolutionary Trees from DNA Sequences: A Maximum Likelihood Approach《从DNA序列推断进化树:一种最大似然方法》。这篇论文研究的核心问题是:如果给定一棵候选进化树和一个DNA变化概率模型,观察到当前这些DNA序列的可能性究竟有多大? [PubMed]又过了六年。1987年7月,两位在美国得克萨斯州休斯敦工作的研究者,提出了另一种更加直接的算法。他们是:斋藤成也(Naruya Saitou)。根井正利(Masatoshi Nei)。论文题目是:The Neighbor-Joining Method: A New Method for Reconstructing Phylogenetic Trees《邻接法:一种重建系统发育树的新方法》。发表在《Molecular Biology and Evolution》第4卷第4期,第406—425页。论文的第一行,就明确指出这是一种从进化距离数据重建树的方法。算法从一棵尚未分组的星形树出发。反复选择合适的邻接对象。更新距离。直至得到完整的无根树。 [Oxford Academic][computingbiology.github.io]这项方法被称为:Neighbor-Joining(NJ)。邻接法。它的魅力在于:不必一开始就穷举所有可能的进化树。可以从距离矩阵出发。通过一系列可以复核的计算,逐步建立分支关系。不过,科学研究并没有因此获得一台绝对正确的“祖先关系计算机”。因为DNA可能发生多次替换。不同分支的演化速度可能不同。不同时期的基因历史,也不一定完全等于物种本身的历史。而有限的序列数据,可能支持不止一种候选树。所以,这一章真正要回答的,不只是:怎样画出一棵进化树?而是:我们如何从有限的分子证据中,计算一棵有根据、可检验,并且明确保留不确定性的生命家谱?

第一幕:两种生物很相似,就一定具有最近的共同祖先吗?

先看一个生物学上非常容易令人误判的现象。海豚与鲨鱼都拥有适合在水中运动的流线形身体。它们都有鳍状结构。都能够快速游泳。如果我们只比较外部形状,很可能会觉得它们是一对非常接近的生物。然而,海豚属于哺乳动物。鲨鱼属于软骨鱼类。它们相似的体形,在很大程度上反映了水生环境中相似的运动需求,而不意味着两者是最近的亲缘分支。这种现象叫作:趋同演化(Convergent Evolution)。它告诉研究者:两个物种拥有某项相似特征,并不自动意味着这项相似来自最近的共同祖先。

那么DNA能够完全解决这个问题吗?

也不能。DNA比较提供了新的证据,但分子序列同样可能出现:平行变化。回复突变。不同分支独立发生相同替换。以及某些受到强烈选择约束而高度保守的位置。因此,科学家需要区分:观察到的相似性。与:推断出的共同进化历史。这两个概念有关联,却不是一回事。

一棵进化树由什么组成?

我们先认识最基本的结构。一棵系统发育树通常包含:叶节点(Tips),表示当前研究的物种、样本或序列。内部节点(Internal Nodes),表示推断出来的共同祖先分叉关系。分支(Branches),连接不同节点。有时还会包含分支长度,用来表示估计的进化变化量。
图55-1:四个对象组成的无根进化树ABCD内部枝A、B位于同侧;C、D位于另一侧这是一棵无根、四叶的教学树,表达AB与CD之间的分裂关系。它没有指定最古老的祖先节点,也不表达哪个现代物种是另一个现代物种的祖先。

无根树与有根树,有什么不同?

这一点是系统发育研究最容易忽视的区别之一。**无根树(Unrooted Tree)**只表达对象之间的分支连接结构。**有根树(Rooted Tree)**还明确指定了共同祖先方向。例如,一棵无根树:
text
A、B | C、D
告诉我们:A和B在一个分支侧。C和D在另一侧。但它并没有告诉我们,所有对象最早从哪个位置开始分化。如果要指定根,研究者通常需要额外信息。例如:适当的外群(Outgroup)。可信的分子钟假设。或者其他独立的进化信息。因此:NJ默认推断的是无根树。不能因为最后把树画成上下分叉的形式,就以为算法已经自动确定了真正的祖先方向。

为什么树的分支长度很重要?

考虑两种树。第一种:所有末端分支都很短。第二种:某个末端分支特别长。它们即使拥有相同的分支连接关系,也可能对应非常不同的进化距离。在分子系统发育分析中,分支长度经常表示:预期的每位点替换次数(Substitutions per Site)。但如果使用其他输入距离,分支长度也可能采用不同单位。因此,我们必须查看具体模型与软件定义。最重要的是:分支长度不自动等于进化时间。只有在满足合适的速率假设和校准条件时,才可能将分子变化量进一步换算成时间。

第二幕:1965年,蛋白质开始成为记录演化历史的分子档案

现在,我们要回答一项更早的问题。科学家为什么开始相信:DNA与蛋白质序列,可以为重建进化关系提供证据?

分子序列带来了新的比较方式

在第51章,我们已经讲过:桑格团队逐步测定蛋白质的完整氨基酸顺序。戴霍夫及其合作者,将越来越多蛋白质序列整理成可计算的资料。这些成果不仅让研究者能够研究单一蛋白质的结构。还打开了另一条道路。比较不同物种中的同源蛋白质。例如:两个物种都拥有某种共同来源的蛋白质。研究者可以比较相应位置上有哪些氨基酸发生变化。随着演化过程积累,这些变化有可能携带关于分支历史的信息。

1965年的一篇重要论文

1965年3月,研究者:埃米尔·祖克坎德尔(Émile Zuckerkandl)。以及:莱纳斯·鲍林(Linus Pauling)。发表:Molecules as Documents of Evolutionary History《分子作为进化历史的文献》。发表于《Journal of Theoretical Biology》第8卷第2期,第357—366页。[PubMed]这篇论文的标题非常贴切。分子并不是历史书。但不同物种中保存下来的序列变化,可能保留着某些演化过程留下的统计痕迹。

什么是分子钟?

在某些条件下,特定分子序列的替换积累速度,在相应演化时间尺度上可能近似稳定。如果研究者能够估计:序列变化量。以及合理的平均替换速率。就有可能推断分支分化所经历的时间。这类思想通常称为:分子钟(Molecular Clock)。但是,“分子钟”不意味着:所有基因、所有物种、所有时期,DNA替换速度都完全相同。不同基因受选择约束的程度可能不同。不同谱系的替换速率也可能不同。因此,使用分子钟推断演化时间,需要特定的模型假设和适当的校准依据。

一个必要的数学区别

假设两个物种之间,观察到的对应核苷酸差异比例是:pp它只是:观察差异比例(Observed p-distance)。但如果一个位置在漫长演化过程中经历:A变成G。后来又从G变成A。那么我们今天看到的两个字符可能完全相同。这个位置却并不是“从来没有发生过变化”。因此:观察到的不同字符数量,不一定等于真实发生过的替换事件数量。当演化距离较大时,多个替换事件可能被掩盖。这就是为什么系统发育算法常常需要先使用概率模型,对观察差异进行修正。下一幕,我们就从这个最基本的距离计算开始。

第三幕:1969年,怎样把DNA字符差异转换成进化距离?

假设科学家已经获得四个物种中某个具有共同进化来源的DNA片段。第一步并不是立刻画树。而是先利用第51章的序列比对方法,确定哪些核苷酸位置应该彼此对应。例如:
text
物种A  A C G T A C G T
物种B  A C G T G C G T
物种C  G C A T A C A T
物种D  G C A T G C A T
这些是人工构造的教学序列。四条序列已经按对应位置排列。我们现在可以逐列比较。

最简单的距离:p-distance

假设两条已经正确对齐的序列,包含:LL个可比较的位置。其中:DD个位置不同。那么,最简单的差异比例是:p=DL\boxed{p=\frac DL}例如,物种A与B只有第五个位置不同。所以:p(A,B)=18=0.125p(A,B)=\frac18=0.125即:12.5%。这叫作未经替换过程修正的观察距离。它很容易计算。但是,它存在一个重要局限。

观察到一次差异,不代表只发生过一次替换

考虑某个祖先位置:
text
A
在一条演化分支上,可能经历:
text
A → G → C
今天我们观察到:
text
C
但实际上已经发生了两次替换。如果又经历:
text
A → G → A
那么,最终字符甚至恢复成了A。我们从两个末端序列看到相同字符,却可能看不到历史上发生过的替换。这意味着:随着演化时间增长,观察差异比例可能逐渐低估真实的替换积累量。

Jukes–Cantor模型:把随机替换纳入距离计算

1969年,Thomas H. Jukes与Charles R. Cantor提出了一个著名的核苷酸替换模型,今天通常简称:JC69模型。它是一种高度简化的DNA替换模型。假设四种核苷酸具有相同的平衡频率。并且不同核苷酸之间的瞬时替换速率相同。在该模型下,距离修正可以写成:dJC69=−34ln⁡(1−43p)\boxed{ d_{\mathrm{JC69}} = -\frac34 \ln\left(1-\frac43p\right) }其中:pp 是观察到的不同核苷酸比例。而 dd 表示在模型假设下估计的每位点替换次数。例如:p=0.125p=0.125代入:d=−34ln⁡(1−43×0.125)d=-\frac34\ln\left(1-\frac43\times0.125\right)得到约:d≈0.137\boxed{d\approx0.137}所以,观察差异比例为0.125,对应的JC69修正距离略高。

为什么公式不能无限使用?

如果:p≥0.75p\ge0.75那么:1−43p≤01-\frac43p\le0此时上述实数对数公式不再提供有限、有效的距离估计。因为在JC69模型中,经过足够漫长的随机替换,两个独立达到平衡的核苷酸位置,其不同的概率趋近于:34\frac34这叫作:替换饱和(Substitution Saturation)相关问题的一种简单表现。因此,并不是序列越不相似,距离修正就可以无限可靠地计算下去。对于高度分化的序列,我们还需要考虑:模型是否合适?比对位置是否仍然可靠?是否存在组成偏差或速率差异?以及是否已经发生严重的信息饱和?这些因素都可能限制分子系统发育的准确性。

距离矩阵怎样形成?

如果我们拥有四种生物A、B、C、D,就可以对每一对序列计算距离。然后得到一个对称矩阵:D=[0dABdACdADdAB0dBCdBDdACdBC0dCDdADdBDdCD0]D= \begin{bmatrix} 0&d_{AB}&d_{AC}&d_{AD}\\ d_{AB}&0&d_{BC}&d_{BD}\\ d_{AC}&d_{BC}&0&d_{CD}\\ d_{AD}&d_{BD}&d_{CD}&0 \end{bmatrix}对角线为0。因为一条序列与自己之间的差异距离为0。而:dAB=dBAd_{AB}=d_{BA}这便为距离法构造进化树提供了输入。但必须强调:距离矩阵中的各项最好来自有意义、可比较的分子序列与合适的替换模型。不能把一组任意相似度数字,未经审查就称为真实的进化距离。

第四幕:一张四物种距离表,为什么已经包含一棵树的线索?

现在,我们暂时使用一组人工构造的距离。这样可以专注于算法本身,而不用被某个真实基因的复杂替换历史干扰。四个物种:A、B、C、D。距离矩阵:
物种ABCD
A0599
B501010
C91008
D91080
这些数值是专门构造的可加性树距离。它们并非来自真实DNA的某次测量。在这个例子中,距离最小的是:d(A,B)=5d(A,B)=5其次:d(C,D)=8d(C,D)=8我们可能直接猜测:A和B属于一组。C和D属于另一组。但在一般情况下,仅凭最小的一项距离,就不能保证直接得到正确的整棵进化树。原因是:一个物种的末端分支可能很短。另一个物种的末端分支可能很长。单纯寻找全局最短距离,不一定等于寻找正确的邻接结构。为了理解这种区别,先让我们研究这组特别清楚的人工数据。

四点条件:四个物种的关系,为什么可以用三组距离和表达?

对于四个末端物种,一棵没有根、完全二叉的树,只有三种不同的拓扑关系。分别是:AB∣CDAB\mid CDAC∣BDAC\mid BDAD∣BCAD\mid BC它们表示三种不同的内部边分割。我们可以构造三个距离和:S1=d(A,B)+d(C,D)S_1=d(A,B)+d(C,D)S2=d(A,C)+d(B,D)S_2=d(A,C)+d(B,D)S3=d(A,D)+d(B,C)S_3=d(A,D)+d(B,C)代入矩阵:S1=5+8=13S_1=5+8=13S2=9+10=19S_2=9+10=19S3=9+10=19S_3=9+10=19所以:S1=13,S2=S3=19\boxed{S_1=13,\quad S_2=S_3=19}在一张满足可加性条件、内部边长度为正的四叶树中,正确分割对应的和最小,而另外两项和相等。因此,当前数据符合:AB∣CD\boxed{AB\mid CD}这种拓扑关系。

什么叫作可加性距离?

如果一张树的每条边拥有非负长度。任意两个叶节点之间的距离,都等于它们之间唯一树路径上的边长之和。那么,我们说这组距离能够由该树表示。对于四叶树,我们可以画出:
图55-2:一棵能够精确解释全部六项距离的树ABCD23344数字为边的距离单位,不是年份
例如,A与C之间的树路径要经过:A的末端边2。内部边3。C的末端边4。所以:d(A,C)=2+3+4=9d(A,C)=2+3+4=9恰好等于原始距离矩阵。B与D之间的路径:3+3+4=103+3+4=10也与矩阵一致。而:d(C,D)=4+4=8d(C,D)=4+4=8同样一致。因此,这棵树能够精确解释全部六项输入距离。

但真实数据通常没有这么理想

实际分子距离会受到:有限样本误差。序列比对误差。替换模型不匹配。不同分支演化速率变化。以及其他生物学过程影响。所以,真实距离矩阵未必能够被任何一棵树完全精确表示。例如,某些情况下:S2≠S3S_2\ne S_3这意味着严格的四点可加性条件不成立。这时,我们需要一种能够根据近似距离重建合理树形的算法。1987年的Neighbor-Joining,正是这类方法中的经典代表。

第五幕:为什么最短距离不一定是最佳邻居?

在1987年的论文中,斋藤成也和根井正利首先定义:Neighbor(邻居)指的是在无根二叉树里,共享同一个直接内部节点的一对叶对象。例如:
text
    A
     \
      ●
     /
    B
A和B是邻居。但关键问题是:怎样判断谁是真正的邻居?

为什么不能简单地找距离最小的一对?

假设A、B、C、D四个物种的演化速率不同。A拥有一条非常短的末端分支。C也拥有一条很短的末端分支。即使A与C不是真正共享最近内部节点的邻居,它们的总体距离也可能相对较小。而某对真正相邻的叶节点,可能因为其中一个分支经历了较多替换,导致两者之间的距离较大。因此,直接寻找:min⁡d(i,j)\min d(i,j)并不总能可靠地恢复树形结构。

NJ的核心改变

NJ没有直接选择最小的:d(i,j)d(i,j)而是考虑:这两个对象与其他对象之间的总体距离。在目前还剩:nn个对象时,定义:ri=∑kd(i,k)\boxed{ r_i=\sum_k d(i,k) }然后构造:Q(i,j)=(n−2)d(i,j)−ri−rj\boxed{ Q(i,j)=(n-2)d(i,j)-r_i-r_j }最后选择:(i,j)=arg⁡min⁡i≠jQ(i,j)\boxed{ (i,j)=\arg\min_{i\ne j}Q(i,j) }注意:Q(i,j)Q(i,j) 并不是新的物种间进化距离。它是为了选择邻接对象而构造的判别量。1987年的原始论文从比较候选树总分支长度的思想出发,推导了对应的邻接选择规则;现代教材通常使用上述Q矩阵形式表达同一核心步骤。[computingbiology.github.io]

为什么要减去两个对象的距离总和?

直观地说:我们不仅要知道i和j彼此有多近。还要考虑它们各自距离其余对象整体有多远。这种校正有助于减少某些末端分支长度差异带来的影响。但不能将它理解为:“所有真实数据经过Q校正,结果就一定正确。”NJ是一种距离驱动的逐步构树方法。它在可加性距离等适当条件下具有很好的数学性质。但当距离估计存在较大误差或模型严重失配时,仍然可能推断出错误拓扑。

1987年论文其实非常谨慎

斋藤与根井没有声称:NJ永远找到全局最短的进化树。原文明确说明,它并不保证在任意输入下都产生全局最小进化树。但他们通过计算机模拟,将NJ与当时的多种构树方法进行比较,发现它具有较好的准确性和效率。[Oxford Academic]因此,NJ的历史价值不只是提出了一条简洁公式。更在于:它把一个规模迅速增长的组合搜索问题,转化成能够反复更新距离矩阵的高效构树过程。

第六幕:亲手计算1987年的Neighbor-Joining——第一次选择,为什么是A和B?

现在,我们终于开始完整执行NJ算法。输入仍然是这张距离矩阵:
ABCD
A0599
B501010
C91008
D91080
当前一共有:n=4n=4个对象。

第一步:计算每个对象的距离总和

对于A:rA=5+9+9=23r_A=5+9+9=23对于B:rB=5+10+10=25r_B=5+10+10=25对于C:rC=9+10+8=27r_C=9+10+8=27对于D:rD=9+10+8=27r_D=9+10+8=27因此:(rA,rB,rC,rD)=(23,25,27,27)\boxed{ (r_A,r_B,r_C,r_D)=(23,25,27,27) }

第二步:计算Q矩阵

根据:Q(i,j)=(n−2)d(i,j)−ri−rjQ(i,j)=(n-2)d(i,j)-r_i-r_j由于:n=4n=4所以:n−2=2n-2=2首先计算A与B:Q(A,B)=2×5−23−25Q(A,B)=2\times5-23-25得到:Q(A,B)=−38\boxed{Q(A,B)=-38}再计算A与C:Q(A,C)=2×9−23−27Q(A,C)=2\times9-23-27得到:Q(A,C)=−32\boxed{Q(A,C)=-32}依次计算所有组合,得到:
Q矩阵ABCD
A—−38−32−32
B−38—−32−32
C−32−32—−38
D−32−32−38—
最小值为:−38\boxed{-38}出现于:A与B。以及:C与D。这两对实际上对应同一条四叶无根树内部边的两个端点分组。对于四个对象的对称距离矩阵,NJ的Q值会在互补的两对中相等。因此,这里选择A、B合并,或者选择C、D合并,最后都可以得到相同的无根拓扑关系。我们采用:A+B→U\boxed{A+B\rightarrow U}其中U表示新建的内部节点。

第三步:计算A与B到内部节点U的距离

NJ不仅要知道哪两个对象应该连接。还要估计它们各自的分支长度。公式为:LAU=d(A,B)2+rA−rB2(n−2)\boxed{ L_{AU} = \frac{d(A,B)}2+ \frac{r_A-r_B}{2(n-2)} }代入:LAU=52+23−252×2L_{AU} = \frac52+\frac{23-25}{2\times2}所以:LAU=2.5−0.5L_{AU}=2.5-0.5得到:LAU=2\boxed{L_{AU}=2}对于B:LBU=d(A,B)−LAUL_{BU}=d(A,B)-L_{AU}因此:LBU=3\boxed{L_{BU}=3}这说明,虽然A与B彼此相邻,但它们到共同内部节点的长度并不一定相同。

第四步:计算新节点U到其余对象的距离

现在,我们希望从四个对象减少到三个对象:U、C、D。对于尚未合并的对象k,更新距离:d(U,k)=d(A,k)+d(B,k)−d(A,B)2\boxed{ d(U,k) = \frac{d(A,k)+d(B,k)-d(A,B)}2 }计算U与C:d(U,C)=9+10−52=7d(U,C)=\frac{9+10-5}{2}=7计算U与D:d(U,D)=9+10−52=7d(U,D)=\frac{9+10-5}{2}=7C与D之间的距离仍然为8。得到新的三对象矩阵:
UCD
U077
C708
D780

第五步:最后三个节点怎样连接?

只有三个末端对象时,一棵无根树的连接结构已经确定。设最后的中心内部节点为V。那么:LUV=d(U,C)+d(U,D)−d(C,D)2L_{UV} = \frac{d(U,C)+d(U,D)-d(C,D)}2代入:LUV=7+7−82=3L_{UV} = \frac{7+7-8}{2}=3同样:LCV=7+8−72=4L_{CV} = \frac{7+8-7}{2}=4LDV=7+8−72=4L_{DV} = \frac{7+8-7}{2}=4因此:LUV=3,LCV=4,LDV=4\boxed{L_{UV}=3,\quad L_{CV}=4,\quad L_{DV}=4}加上此前的:LAU=2L_{AU}=2LBU=3L_{BU}=3整棵无根树便构造完成了。

第六步:检查全部输入距离是否恢复

最好的办法不是直接相信算法的输出。而是把树上路径的边长重新加起来。例如:d(A,B)=2+3=5d(A,B)=2+3=5d(A,C)=2+3+4=9d(A,C)=2+3+4=9d(B,D)=3+3+4=10d(B,D)=3+3+4=10d(C,D)=4+4=8d(C,D)=4+4=8全部与输入距离矩阵一致。因此,对于这组人工构造的可加性距离,NJ精确恢复了原来的树结构及分支长度。

这项算法需要多少计算?

如果每轮都直接计算完整Q矩阵:需要处理约:O(n2)O(n^2)个对象对。然后合并一对对象,减少一个节点。整个过程重复约:nn轮。所以,朴素NJ实现的时间复杂度为:O(n3)\boxed{O(n^3)}距离矩阵需要:O(n2)\boxed{O(n^2)}空间。虽然真实的大规模NJ实现还可以使用不同的加速策略,但与穷举全部树形相比,这种多项式时间算法已经是一项巨大改善。

第七幕:互动实验室——为什么四种生物能够组成三棵不同的树?

现在,我们直接操作四物种的距离矩阵。你可以切换:AB与CD分为两侧。AC与BD分为两侧。AD与BC分为两侧。还可以人为加入一部分距离测量误差。我们将同时计算:六个物种对之间的距离。三组四点距离和。NJ的Q判别矩阵。以及最终选出的拓扑。

互动实验:让距离决定树形

图55-3:四物种距离与Neighbor-Joining实验台选择一组真实可由树边长度相加而来的教学距离,再加入测量扰动,观察四点条件与NJ判别量如何改变。

+0

扰动为人为加入的距离误差,并不是DNA演化模拟。扰动后矩阵通常不再严格满足可加性树条件。
距离矩阵D
 ABCD
A0599
B501010
C91008
D91080
三组四点距离和
AB | CD
13
AC | BD
19
AD | BC
19
NJ的Q值
候选对原始距离Q判别量
A—B5-38
A—C9-32
A—D9-32
B—C10-32
B—D10-32
C—D8-38
根据当前距离推断的无根拓扑ABCD内部边拓扑:AB | CD
NJ选出的四叶分组

AB | CD

三项距离和中的最小值13
当前距离来自一棵严格可加的教学树,NJ可以恢复对应拓扑与原来的边长度。
四个叶节点时,NJ的最优Q值成互补对出现,因此其分组选择可用最小四点距离和直观说明;一般有更多物种时,不能只用这一项四点简化规则代替完整的NJ迭代算法。本图的线条长度不是按输入距离比例绘制的。

实验一:切换三种真正不同的拓扑

首先选择:AB | CD你会得到:SAB∣CD=13S_{AB|CD}=13另外两项为19。然后切换:AC | BD你会看到最小距离和转移到另一组。最后选择:AD | BC观察四点条件与Q矩阵怎样变化。这说明:进化树不只是将同一批物种标签随意排列成不同的图。每一种拓扑,都对应不同的路径距离关系。

实验二:人为加入测量误差

把A—C的距离增加:1, 2, 3, 41,\ 2,\ 3,\ 4观察原本相等的另外两项四点距离和。它们通常不再完全相等。这意味着:当前矩阵可能已经不再由一棵严格可加的树精确表示。但NJ仍然能够根据Q判别量,选择一个拓扑。这正是实际系统发育研究中经常面对的情形。输入数据存在噪声,而算法仍然需要给出一种合理的树形估计。但是,得到一棵树并不等于证明它就是唯一正确的演化历史。

第八幕:1958年的UPGMA,为什么能画树,却容易误解演化速度?

在Neighbor-Joining出现之前,研究者已经拥有许多聚类方法。其中一项经典方法是:UPGMA。全称:Unweighted Pair Group Method with Arithmetic Mean。中文通常译作:非加权组平均法。UPGMA与Sokal和Michener在1958年发展的数值分类研究密切相关。它通过距离矩阵,将对象逐步合并成层次聚类结构。[Oxford Academic]

UPGMA怎样构造树?

它的步骤看起来很简单。首先,找出当前距离最小的两个对象或簇。将它们合并。然后,用簇内所有成员之间的平均距离,计算新簇与其他簇的距离。重复执行。直到全部对象被合并。例如,对于两个簇:UU和:VV它们之间的平均距离定义为:d(U,V)=1∣U∣∣V∣∑i∈U∑j∈Vd(i,j)\boxed{ d(U,V)= \frac{1}{|U||V|} \sum_{i\in U}\sum_{j\in V}d(i,j) }其中:∣U∣|U|表示簇U中包含的原始对象数量。注意,UPGMA虽然名称里有“Unweighted”,但不能理解成合并新簇时完全忽略簇中有多少成员。它使用的是原始对象对之间的算术平均。

用同一份数据运行UPGMA

仍然使用:d(A,B)=5d(A,B)=5d(C,D)=8d(C,D)=8其他跨组距离是:9, 9, 10, 109,\ 9,\ 10,\ 10第一步,合并最近的A和B。它们的连接高度为:hAB=52=2.5h_{AB}=\frac52=2.5第二步,合并C和D。高度:hCD=82=4h_{CD}=\frac82=4第三步,计算两个簇之间的平均距离:d(AB,CD)=9+9+10+104d(AB,CD) = \frac{9+9+10+10}{4}得到:9.5\boxed{9.5}所以最终根节点的高度:hroot=9.52=4.75h_{\mathrm{root}}=\frac{9.5}{2}=4.75这时,一棵有根树便形成了。
图55-4:UPGMA在相同距离矩阵下产生的有根树
ABCD根2.54.0所有叶节点距离根的高度均为4.75
查看 SVG 配图原件
示意图原件 55-012-b12fac30ccd4.svg
55-012-b12fac30ccd4.svg · 点击查看大图
这是一张结构示意图,竖向画线长度未严格按比例绘制。UPGMA按簇高度构造有根、超度量树;本例中不同叶节点的原始距离不能被其全部精确复现。

为什么UPGMA的全部末端叶节点到根的距离相等?

因为它把每次合并发生的高度定义成:簇间距离2\frac{\text{簇间距离}}2在生成的树中,现存对象的末端被放在相同的比较高度。因此,UPGMA构造的是:Ultrametric Tree(超度量树)。这种结构在理想化情况下,与不同谱系具有相同分子变化速率的假设相容。也就是一种严格分子钟式的模型。

可是我们的原始距离并不满足这样的假设

回想NJ恢复出的边长:A到内部节点为2。B到同一内部节点为3。它们的末端边长度并不相同。而UPGMA将A与B合并后,它们到共同内部节点的长度都设成:2.52.5这意味着,虽然两种方法在当前例子中得到了相同的无根分组关系:AB∣CDAB\mid CD它们给出的分支长度与根的位置,却不是同一回事。对于不同谱系演化速率存在明显差异的数据,UPGMA甚至可能得到错误的分支拓扑。这也是Neighbor-Joining相对于简单平均聚类方法的重要优势之一。

1987年的论文比较过UPGMA

Saitou与Nei在原始论文中,将NJ与包括UPGMA在内的几种构树方法进行计算机模拟比较。他们并没有说UPGMA在任何情况下都没有价值。而是发现,在一些不满足严格分子钟等条件的模拟场景中,NJ具有更好的恢复正确树形的表现。[Oxford Academic]这说明:聚类方法的计算速度与表达能力,必须与数据背后的演化假设一起考虑。不能因为两种算法都能输出一棵树,就认为它们是在使用完全相同的科学模型。

第九幕:1967年的Fitch与Margoliash,以及一项后来被重新审视的原则——最少变化就是最佳解释吗?

1967年1月20日,《Science》发表:Construction of Phylogenetic Trees《系统发育树的构造》。作者是:沃尔特·菲奇(Walter M. Fitch)。以及:伊曼纽尔·马戈利亚什(Emanuel Margoliash)。论文刊登于第155卷第3760期,第279—284页。[PubMed]这项研究与蛋白质序列差异及距离法构树的发展有关,特别是细胞色素c等分子资料在进化关系研究中的应用。

为什么“最少变化”具有吸引力?

想象我们有四条DNA序列。某个位置的字符是:
text
物种A  A
物种B  A
物种C  G
物种D  G
如果树形是:AB∣CDAB\mid CD那么,我们可以想象:A和B一侧的祖先拥有A。C和D一侧的祖先拥有G。两侧之间只需要一次替换,就能解释全部四个末端观测。但是,如果我们选择:AC∣BDAC\mid BD那么,A和G被放在同一侧,B和D也被放在同一侧。为了同时解释这些末端状态,往往需要更多替换事件。这种直觉形成了一类重要的方法:Maximum Parsimony(最大简约法)。它寻找能够以尽可能少的演化变化解释观测数据的树。不过,必须区分两项研究历史。Fitch与Margoliash在1967年的论文主要属于根据分子差异进行树构造和距离拟合的研究路线。而最大简约法具有另外的、逐渐形成的系统发育方法史,不能简单说“1967年Fitch–Margoliash论文就是全部最大简约法的起点”。

为什么最少变化不一定代表真实历史?

假设两个生物谱系具有很高的替换速率。它们各自独立发生了大量突变。其中有些位置可能碰巧变成相同字符。如果我们只追求能够以最少替换解释现有观测的树形,就可能把这两条高速变化的谱系错误地放在一起。这类问题经常与:Long-Branch Attraction(长枝吸引)联系起来。在某些演化条件下,最大简约法可能受到长分支上独立同形变化的严重干扰。Felsenstein在1978年对这类方法的统计性质进行了重要研究。而1981年的最大似然论文,则将不同谱系可能具有不同进化速率的问题,纳入了概率模型框架。[PubMed]

这里最重要的科学区别

简约法问:需要多少次最少的变化,才能解释这些观测?最大似然法问:在指定的概率替换模型与树结构下,出现当前这些观测的概率有多大?二者不是同一个优化目标。最大似然方法也不会因为引入概率,就在所有错误模型和有限数据条件下自动正确。但它提供了一种更明确的统计框架:允许研究者根据假定的替换过程,计算观察数据的可能性,并比较不同树形。这个思想,在1981年迎来了一项重要突破。

第十幕:1981年,Felsenstein让整棵进化树拥有了可计算的概率

1981年,Joseph Felsenstein在《Journal of Molecular Evolution》发表:Evolutionary Trees from DNA Sequences: A Maximum Likelihood Approach论文第17卷第6期,第368—376页。这篇论文不仅提出了一种研究进化树的概率观点。更重要的是,它发展了实际可计算的方法,用于评估DNA序列在候选进化树与替换模型下的似然,并搜索更优的树和分支长度。[PubMed][PMC]

为什么直接计算树的概率很困难?

假设我们已经观察到四种生物的DNA。但它们共同祖先位置上的核苷酸,并没有直接保存下来。我们只能观察末端物种的字符。而一棵树的内部节点,可能取:A、C、G、T。四种状态中的任意一种。如果存在多个内部节点,候选祖先状态组合数量会迅速增加。例如,有:kk个未知祖先核苷酸状态。直接枚举全部组合,就有:4k4^k种可能。如果对每条候选进化树、每个DNA位置都这样处理,计算量就非常昂贵。

Felsenstein的关键思想:沿树自底向上合并概率

这是一种动态规划。今天常称为:Felsenstein Pruning Algorithm(费尔森斯坦剪枝算法)。注意,这里的Pruning不是把生物学上不重要的树枝直接删除。而是指通过递推组织概率计算,避免重复枚举全部内部祖先状态组合。

先定义一条边上的替换概率

使用刚才介绍的JC69模型。假设边长:tt单位为预期每位点替换次数。那么:从某种碱基保持不变的概率是:Psame(t)=14+34e−4t/3\boxed{ P_{\mathrm{same}}(t) = \frac14+\frac34e^{-4t/3} }从一种碱基变成某个特定其他碱基的概率是:Pdifferent(t)=14−14e−4t/3\boxed{ P_{\mathrm{different}}(t) = \frac14-\frac14e^{-4t/3} }例如,如果祖先是A。那么,在一条长度为t的分支末端:成为A的概率为:Psame(t)P_{\mathrm{same}}(t)成为C、G或T中某个特定字符的概率分别为:Pdifferent(t)P_{\mathrm{different}}(t)这就把分支长度和核苷酸变化的概率联系起来了。

定义内部节点的条件似然

设:Lv(s)L_v(s)表示:如果节点v的核苷酸为s,那么以v为根的子树中,观察到全部叶节点字符的条件概率。如果v是叶节点,并且观测字符为A:Lv(A)=1L_v(A)=1其他三种状态:Lv(C)=Lv(G)=Lv(T)=0L_v(C)=L_v(G)=L_v(T)=0这是没有测序错误、没有模糊碱基的简化情况。对于内部节点v,假设它有两个子节点c₁和c₂。我们需要对每个子节点可能的核苷酸状态求和。所以:Lv(s)=[∑xPsx(t1)Lc1(x)][∑yPsy(t2)Lc2(y)]\boxed{ L_v(s)= \left[\sum_xP_{sx}(t_1)L_{c_1}(x)\right] \left[\sum_yP_{sy}(t_2)L_{c_2}(y)\right] }它意味着:先分别计算左子树与右子树在当前祖先状态s下的概率。再将两侧的条件概率相乘。这样,就不必重复枚举各子树内部全部状态组合。

最后怎样得到整个位点的概率?

假设根节点核苷酸的先验分布为:πs\pi_s那么:P(观测位点∣T,θ)=∑sπsLroot(s)\boxed{ P(\text{观测位点}\mid T,\theta) = \sum_s\pi_sL_{\mathrm{root}}(s) }在JC69的平衡分布下:πA=πC=πG=πT=0.25\pi_A=\pi_C=\pi_G=\pi_T=0.25因此:P(观测位点∣T,θ)=14∑sLroot(s)P(\text{观测位点}\mid T,\theta) = \frac14\sum_sL_{\mathrm{root}}(s)

整段DNA怎么办?

如果模型假设各个比对位点条件独立,那么:P(D∣T,θ)=∏k=1LP(Dk∣T,θ)P(D\mid T,\theta) = \prod_{k=1}^{L}P(D_k\mid T,\theta)由于连续乘积可能非常小,实际程序通常使用对数似然:ℓ(T,θ)=∑k=1Llog⁡P(Dk∣T,θ)\boxed{ \ell(T,\theta) = \sum_{k=1}^{L}\log P(D_k\mid T,\theta) }然后,寻找能够使:ℓ(T,θ)\ell(T,\theta)尽可能大的候选树与模型参数。这里的:θ\theta可能包括替换模型参数和分支长度。而不是简单地固定所有分支,然后只比较三种树形。

为什么这与第53章的Forward算法如此相似?

因为它们都在做一件事。对看不见的隐藏状态,利用动态规划求和。第53章:隐藏状态沿着时间线发展。所以,我们从左到右运行Forward。本章:祖先核苷酸状态沿着进化树分支展开。所以,我们从叶节点向内部节点计算条件似然。两者的图形结构不同。但概率递推的思想非常接近。
图55-5:两种利用动态规划处理隐藏状态的方法
HMM的Forward沿时间方向累积所有隐藏状态路径的概率。
树上的PruningAAGG沿树从叶向内,累积所有可能的祖先状态概率。

最大似然是否需要穷举所有树?

对于很少的物种,可以枚举全部候选拓扑。但物种数量增加以后,完全枚举通常不现实。现代最大似然构树程序经常使用:局部树形调整。分支长度优化。模型参数估计。以及不同的搜索启发式。因此,最大似然中的“给一棵树算似然”和“寻找似然最高的树”,是两个不同层次的问题。Felsenstein的剪枝递推,使前一项计算变得高效。但并不意味着后一项全局组合优化,已经自动变成一个总能在多项式时间内找到全局最优解的简单问题。

第十一幕:1985年,Felsenstein提出一个新的问题——这棵树究竟有多可靠?

现在,假设我们拥有四个物种的DNA。运行Neighbor-Joining。得到:AB∣CDAB\mid CD接着,换用另一组DNA位置。可能仍然得到:AB∣CDAB\mid CD也可能得到:AC∣BDAC\mid BD那么,研究者应当如何表达这种不确定性?只输出一棵树,可能会让人误以为:所有分支都已经被充分证明。但真实数据中的某些分支,可能只受到很弱的证据支持。

1985年:系统发育树的Bootstrap

1985年7月,Joseph Felsenstein在《Evolution》发表:Confidence Limits on Phylogenies: An Approach Using the Bootstrap《系统发育树的置信界限:一种Bootstrap方法》。论文第39卷第4期,第783—791页。他将统计学中的重采样思想,应用到系统发育树估计。[Oxford Academic]

Bootstrap究竟是什么?

假设我们拥有一份已经正确对齐的DNA序列矩阵。共有四个物种。每条序列长度为:L=1000L=1000也就是说,拥有1000列可比较的核苷酸信息。Bootstrap不会在每次重复中随机删除某个物种。而是保留全部物种,然后从原来的对齐列中:有放回地抽取1000列。“有放回”意味着:同一列可能被抽中多次。某些原始列可能一次也没有被抽到。于是,形成一份与原始数据具有相同列数、但列组成略有变化的新数据集。接着,使用这份新数据重新构树。然后继续重复。例如:100次。500次。1000次。最后统计:某一个内部树枝或分组关系,在多少次重采样树中出现。

一个例子

假设我们进行了1000次Bootstrap重采样。得到:
无根四叶拓扑出现次数
AB | CD830
AC | BD130
AD | BC40
那么:AB∣CDAB\mid CD的出现比例是:8301000=83%\boxed{\frac{830}{1000}=83\%}我们可以将83%称为这项分组的Bootstrap支持比例。但需要非常谨慎。83%的Bootstrap支持度,不等于这棵树有83%的后验概率是真的。它表达的是:在给定数据、重采样方法和构树程序下,这个分组在重复分析中出现的频率。这是一种稳定性和支持度指标。而不是未经额外统计假设就能直接解释的“真实祖先概率”。

互动实验:哪些DNA列真正支持哪棵树?

我们构造一份完全人工的四物种DNA对齐。其中包含四种类型的列:第一类:
text
A A G G
支持:AB∣CDAB\mid CD第二类:
text
A G A G
支持:AC∣BDAC\mid BD第三类:
text
A G G A
支持:AD∣BCAD\mid BC第四类:
text
A A A A
四个物种完全相同,本身不区分上述三种拓扑。对于这份高度简化、只含两种字符的四叶教学数据,使用四点距离和时,支持某个分组的列越多,该分组越可能成为最优结果。现在,让我们亲手执行重采样。
图55-6:人工DNA列重采样与分支支持度调整有利于AB分组的位点数,并重复有放回抽样,观察最优拓扑在不同重采样数据中的出现频率。

12

位点模式支持分组列数
A A G GAB | CD12
A G A GAC | BD5
A G G AAD | BC3
A A A A不区分拓扑10
人工对齐总长度30列
四叶拓扑出现的频率
AB | CD95.5% · 191次
AC | BD2.0% · 4次
AD | BC0.0% · 0次
并列未定2.5% · 5次
前六份重采样数据的结果
重复编号ABACAD最优分组
11263AB|CD
21834AB|CD
31334AB|CD
41147AB|CD
51244AB|CD
61342AB|CD
这是可复现的教学版列Bootstrap。为直观展示四点分组,程序使用人工设计的二元位点模式和距离和判别,不是正式系统发育软件的完整NJ与分支支持度流程。并列次数单独记录,不任意归入某个分组。Bootstrap频率不是拓扑正确的概率。

实验一:让AB的证据非常充分

把支持AB的列数调到:2020另外两种冲突模式只有5列和3列。此时,Bootstrap重采样通常仍然会让AB分组占据优势。因此:AB∣CDAB\mid CD的支持比例往往较高。

实验二:让三种分组的证据接近

把支持AB的列数调到:33此时,三种拓扑的证据变得更加接近。重复抽样以后,不同拓扑可能轮流成为最优。这意味着:当前观测数据尚不足以稳定区分这些候选分支关系。如果研究者只输出其中出现次数最多的一棵树,而不说明支持比例,就容易夸大结果的确定性。

实验三:增加Bootstrap次数

从:50次。增加到:200次。再增加到:1000次。你会发现,频率估计通常会变得更加稳定。但更多次重采样,不意味着原始数据因此变得更加充分。如果数据本身缺少区分树形的信息,那么执行一百万次Bootstrap,也不能凭空创造出新的进化证据。计算重复次数可以降低Monte Carlo抽样误差,却不能弥补原始序列本身的信息不足。

第十二幕:Python实战——不用专业生物信息学库,完整重建一棵Neighbor-Joining树

现在,我们把1987年的NJ算法真正写成程序。不使用:Biopython。SciPy。也不使用现成的系统发育软件。只使用Python标准库。我们的目标不只是输出:
text
AB | CD
而是要恢复:全部内部节点。每条边的长度。以及由这些边组成的完整无根树。

第一步:实现Neighbor-Joining

python

from itertools import combinations


def neighbor_joining(names, matrix):
    """
    根据距离矩阵建立无根Neighbor-Joining树。

    names:
        叶节点名称列表,例如 ["A", "B", "C", "D"]

    matrix:
        对称距离矩阵,对角线为0

    返回:
        graph:
            无根树邻接表
            graph[node] = [(neighbor, branch_length), ...]

        steps:
            每次合并的记录

    说明:
        不对负分支长度进行静默截断。
        若输入不是加性距离,可能出现负长度估计。
    """

    if len(names) < 3:
        raise ValueError("至少需要三个物种")

    if len(matrix) != len(names):
        raise ValueError("距离矩阵维度错误")

    if len(set(names)) != len(names):
        raise ValueError("叶节点名称必须唯一")

    n = len(names)

    for i in range(n):
        if len(matrix[i]) != n:
            raise ValueError("距离矩阵不是方阵")

        if abs(matrix[i][i]) > 1e-9:
            raise ValueError("对角线必须为0")

        for j in range(n):
            if matrix[i][j] < 0:
                raise ValueError("输入距离不能为负")

            if abs(matrix[i][j] - matrix[j][i]) > 1e-9:
                raise ValueError("距离矩阵必须对称")

    d = {
        a: {
            b: float(matrix[i][j])
            for j, b in enumerate(names)
        }
        for i, a in enumerate(names)
    }

    active = list(names)

    graph = {
        name: [] for name in names
    }

    steps = []

    def link(a, b, length):
        graph.setdefault(a, []).append((b, length))
        graph.setdefault(b, []).append((a, length))

    internal_id = 0

    while len(active) > 3:

        size = len(active)

        row_sum = {
            a: sum(d[a][k] for k in active)
            for a in active
        }

        # 选择Q值最小的一对
        def q_value(a, b):
            return (
                (size - 2) * d[a][b]
                - row_sum[a]
                - row_sum[b]
            )

        a, b = min(
            combinations(active, 2),
            key=lambda pair: (
                q_value(pair[0], pair[1]),
                pair
            )
        )

        distance_ab = d[a][b]

        branch_a = (
            distance_ab / 2
            + (row_sum[a] - row_sum[b])
            / (2 * (size - 2))
        )

        branch_b = distance_ab - branch_a

        new_node = f"U{internal_id}"
        internal_id += 1

        link(new_node, a, branch_a)
        link(new_node, b, branch_b)

        steps.append({
            "join": (a, b),
            "q": q_value(a, b),
            "new_node": new_node,
            "branches": (branch_a, branch_b)
        })

        # 为新节点计算到其余对象的距离
        d[new_node] = {}

        for k in active:
            if k in (a, b):
                continue

            new_distance = (
                d[a][k] + d[b][k] - distance_ab
            ) / 2

            d[new_node][k] = new_distance
            d[k][new_node] = new_distance

        # 删除已经合并的两个对象
        for k in active:
            d[k].pop(a, None)
            d[k].pop(b, None)

        d.pop(a)
        d.pop(b)

        active = [
            k for k in active
            if k not in (a, b)
        ]

        active.append(new_node)

    # 最后只剩三个对象
    a, b, c = active

    center = "Ufinal"

    for x, y, z in (
        (a, b, c),
        (b, a, c),
        (c, a, b)
    ):
        length = (
            d[x][y] + d[x][z] - d[y][z]
        ) / 2

        link(center, x, length)

    return graph, steps

第二步:使用本章距离矩阵测试

python

NAMES = ["A", "B", "C", "D"]

DISTANCES = [
    [0, 5, 9, 9],
    [5, 0, 10, 10],
    [9, 10, 0, 8],
    [9, 10, 8, 0],
]

graph, steps = neighbor_joining(
    NAMES,
    DISTANCES
)

print("合并过程:")

for step in steps:
    print(step)

print()
print("无根树的全部边:")

visited = set()

for node, neighbors in graph.items():
    for other, length in neighbors:

        edge = tuple(sorted((node, other)))

        if edge in visited:
            continue

        visited.add(edge)

        print(
            node,
            "<->",
            other,
            "长度:",
            round(length, 6)
        )
按这份输入,关键输出应该包含:
text
A      <-> U0       长度 2
B      <-> U0       长度 3
U0     <-> Ufinal   长度 3
C      <-> Ufinal   长度 4
D      <-> Ufinal   长度 4
其中,内部节点叫什么名字并不具有生物学意义。U0和Ufinal只是程序生成的技术标识。真正有意义的是:哪些末端叶节点通过哪些内部边连接。以及对应的分支长度。

第三步:验证树上的距离

我们不能只因为程序没有报错,就认为输出正确。现在,利用无根树的邻接表,计算任意两个叶节点之间的路径长度。
python

def tree_distance(graph, start, target):
    """
    在已经构造好的树中,
    计算两个节点之间的唯一简单路径长度。
    """

    stack = [(start, None, 0.0)]

    while stack:
        node, parent, distance = stack.pop()

        if node == target:
            return distance

        for neighbor, length in graph[node]:
            if neighbor == parent:
                continue

            stack.append((
                neighbor,
                node,
                distance + length
            ))

    raise ValueError("图中找不到目标节点")


for i, a in enumerate(NAMES):
    for j in range(i + 1, len(NAMES)):
        b = NAMES[j]

        reconstructed = tree_distance(
            graph, a, b
        )

        expected = DISTANCES[i][j]

        assert abs(
            reconstructed - expected
        ) < 1e-9

        print(
            a, b,
            "原距离:", expected,
            "树上距离:", reconstructed
        )

print("全部六项距离恢复正确")
这项测试验证了:对于我们使用的严格可加性距离,NJ构造出的树能够精确恢复所有输入距离。但对于真实数据,即使NJ实现没有错误,输出的树距离也可能与输入矩阵存在偏差。因为真实距离矩阵未必能够由任何一棵树精确表示。

第四步:输出Newick格式

生物信息学软件经常使用一种非常简洁的树结构文本格式:Newick。例如:
text
((A:2,B:3):3,C:4,D:4);
冒号后面的数字表示分支长度。括号表示分支的层次结构。但必须注意:Newick文本在语法上具有一个最外层的表示位置,并不意味着我们已经从生物学上确定了真正的根。对于无根NJ树,选择某个内部节点作为输出的起始位置,通常只是为了方便序列化。下面,我们把无根树转换成一份Newick文本。
python

def to_newick(graph, root="Ufinal"):
    """
    从指定的技术根开始输出Newick。

    注意:
        root只是序列化起点。
        不代表已经确定生物学上的真正祖先根。
    """

    def visit(node, parent):
        children = [
            (other, length)
            for other, length in graph[node]
            if other != parent
        ]

        if not children:
            return node

        parts = []

        for child, length in sorted(children):
            subtree = visit(child, node)

            parts.append(
                f"{subtree}:{length:.6f}"
            )

        return "(" + ",".join(parts) + ")"

    return visit(root, None) + ";"


print(to_newick(graph))
它会得到一份与树结构一致的Newick字符串。后续可以交给其他支持Newick的软件进行可视化或分析。但如果要用于严格的系统发育研究,还需要结合软件约定、树根、分支长度单位及模型信息进行处理。

第十三幕:再写一项程序——给三棵候选树计算最大似然分数

上一段Python程序实现的是:Neighbor-Joining距离法。但我们还没有亲手计算:Felsenstein最大似然方法的核心递推。现在,让我们完成第五篇最后一项重要算法实验。

用四种物种构造人工DNA

我们构造四种位点模式。第一类:
text
A A G G
出现12次。第二类:
text
A G A G
出现5次。第三类:
text
A G G A
出现3次。第四类:
text
A A A A
出现10次。总共:12+5+3+10=3012+5+3+10=30个比对位置。我们希望比较三种无根四叶树:AB∣CDAB\mid CDAC∣BDAC\mid BDAD∣BCAD\mid BC

为什么不能只数哪种位点出现次数最多?

我们当然可以观察:支持AB分组的位点更多。但最大似然方法并不只是简单数票。它希望根据:分支长度。核苷酸替换概率。以及全部祖先状态组合。计算每个候选树生成当前30列观测数据的概率。在这里,我们为了将重点集中于剪枝递推,先人为给定全部候选树相同的分支长度。所有末端边:t=0.08t=0.08内部边的总长度:t=0.20t=0.20并使用JC69替换模型。这些长度的单位是预期每位点替换次数,不是上一节距离矩阵中的抽象整数单位。

使用Python计算条件似然

python

import math

BASES = "ACGT"


def jc69_probability(a, b, branch_length):
    """
    JC69模型:
    在给定分支长度下,
    祖先碱基a变成后代碱基b的概率。
    """

    factor = math.exp(
        -4.0 * branch_length / 3.0
    )

    if a == b:
        return 0.25 + 0.75 * factor

    return 0.25 - 0.25 * factor


def partial_likelihood(node, column, sequences):
    """
    在树上自底向上计算条件似然。

    叶节点:
        用物种名字符串表示。

    内部节点:
        [(child, branch_length), ...]

    返回:
        {A:概率, C:概率, G:概率, T:概率}
    """

    if isinstance(node, str):
        observed = sequences[node][column]

        return {
            base: float(base == observed)
            for base in BASES
        }

    child_results = [
        (
            partial_likelihood(
                child, column, sequences
            ),
            branch_length
        )
        for child, branch_length in node
    ]

    result = {}

    for ancestral_base in BASES:

        probability = 1.0

        for child_scores, length in child_results:

            subtotal = sum(
                jc69_probability(
                    ancestral_base,
                    descendant_base,
                    length
                ) * child_scores[descendant_base]

                for descendant_base in BASES
            )

            probability *= subtotal

        result[ancestral_base] = probability

    return result


def tree_log_likelihood(
    sequences,
    left_pair,
    right_pair,
):
    """
    对指定四叶拓扑计算观测对数似然。

    固定分支长度:
        末端边0.08
        内部总边长0.20

    这里只比较固定参数下的树。
    尚未进行分支长度优化。
    """

    def clade(pair):
        return [
            (pair[0], 0.08),
            (pair[1], 0.08),
        ]

    # 将内部边在计算表示上拆成两段0.10
    root = [
        (clade(left_pair), 0.10),
        (clade(right_pair), 0.10),
    ]

    length = len(next(iter(sequences.values())))

    total_loglik = 0.0

    for column in range(length):

        root_scores = partial_likelihood(
            root, column, sequences
        )

        # JC69平衡频率:A/C/G/T各为0.25
        site_probability = sum(
            root_scores.values()
        ) / 4.0

        total_loglik += math.log(
            site_probability
        )

    return total_loglik

构造人工数据并比较三棵树

python

patterns = (
    [("A", "A", "G", "G")] * 12
    + [("A", "G", "A", "G")] * 5
    + [("A", "G", "G", "A")] * 3
    + [("A", "A", "A", "A")] * 10
)

sequences = {
    taxon: "".join(
        column[i]
        for column in patterns
    )
    for i, taxon in enumerate("ABCD")
}


topologies = [
    (("A", "B"), ("C", "D")),
    (("A", "C"), ("B", "D")),
    (("A", "D"), ("B", "C")),
]

results = []

for left, right in topologies:
    score = tree_log_likelihood(
        sequences, left, right
    )

    label = (
        "".join(left)
        + " | "
        + "".join(right)
    )

    results.append((score, label))

    print(
        label,
        "log-likelihood:",
        round(score, 4)
    )


best = max(results)

print()
print("固定参数下得分最高的拓扑:", best[1])
在这组固定分支长度与JC69模型下,结果约为:
候选树对数似然
AB | CD−139.7202
AC | BD−166.3575
AD | BC−173.9682
因此:AB∣CD\boxed{AB\mid CD}获得最大的对数似然。注意:−139.7202-139.7202比:−166.3575-166.3575大。因为负数越接近零,数值越大。

这算真正的最大似然构树程序吗?

还不完全算。它实现了:给定模型和固定分支长度时,计算候选树的似然。但一个完整的最大似然构树程序,还应该考虑:为每种拓扑优化分支长度。在适当情况下优化替换模型参数。检查不同初始条件。搜索更多候选树。以及评估结果的不确定性。如果仅比较固定分支长度下的似然,就不能声称已经为每种拓扑完成了真正意义上的最大似然优化。不过,这段程序已经展示了1981年Felsenstein方法最重要的计算核心:利用树上的动态规划,把对全部隐藏祖先状态的指数级直接枚举,组织成可以逐节点递推的概率计算。

第十四幕:一棵基因树,为什么不一定等于物种真正的家谱?

到现在,我们已经学会了两种重要的构树方法。Neighbor-Joining。最大似然。还学会了:如何使用Bootstrap评估某些分支在数据重采样中的稳定性。但最后,仍然有一个不能回避的科学问题。我们到底在重建什么树?

基因树与物种树

假设我们分别从四种动物中获取一个同源基因。将这些基因进行序列比对。然后运行NJ或最大似然方法。得到一棵树。这棵树首先表达的是:Gene Tree(基因树)。也就是基于相关基因序列证据,推断这些基因副本之间的演化关系。但生物学家往往真正关心的是:Species Tree(物种树)。即:物种或种群分化的历史关系。两者有关联,却并不总是完全相同。

为什么同一个物种可能拥有不同历史的基因?

首先是基因复制。假设某个共同祖先拥有一个基因。后来,该基因在祖先谱系中发生复制。于是产生两个不同的基因副本。后代物种可能同时保留这些副本。如果我们不小心将两个来源于古老复制事件的基因副本,直接拿来当作一对对应直系同源基因比较,就可能推断出不代表真实物种分化顺序的关系。因此,现代分子系统发育研究通常需要认真区分:Ortholog(直系同源基因)。以及:Paralog(旁系同源基因)。它们都可能具有共同进化来源,但形成对应关系的具体历史不同。

还有一种更深刻的现象:不完全谱系排序

假设三个物种在相对短的演化时间内连续分化。祖先种群中,某些基因变异可能已经存在。但不同基因谱系在更早的祖先种群中,并不一定按照物种分化事件的顺序立即合并。于是,不同基因位点可能保留不同的谱系关系。这种现象叫作:Incomplete Lineage Sorting(ILS,不完全谱系排序)。因此,同一组物种的不同基因,有时可能支持不同的基因树。这种差异并不自动意味着:其中所有不同于物种树的基因树都存在测序错误。它可能反映真实的群体遗传历史。

微生物还会遇到水平基因转移

对于许多微生物,遗传信息不一定只沿着亲代到子代的垂直谱系传递。某些基因可以通过:Horizontal Gene Transfer(水平基因转移)在不同谱系之间传播。因此,如果研究者只使用某个发生过水平转移的基因,就可能推断出一棵反映该基因转移历史、而非整个物种分化历史的树。这也是为什么,不能把生命所有演化过程都简单地理解成一棵毫无交叉的严格分支树。系统发育树仍然非常重要。但某些复杂历史还需要使用:系统发育网络。多基因模型。共祖过程模型。或者其他更适合表达网状关系和多谱系过程的方法。

从一条DNA走向生命家谱,需要怎样的证据链?

图55-7:从分子资料到物种关系推断
1
确定比较对象选择具有合理共同演化来源的基因或序列区域

2
建立可靠序列比对确保用于比较的列具有合理的位置对应关系

3
估计演化模型考虑替换频率、速率差异、分支长度及可能的饱和

4
构造候选基因树使用距离法、最大似然或其他合适的统计方法

5
评估分支支持度借助Bootstrap或其他方法分析有限数据下的不确定性

6
解释物种历史结合多个基因、群体遗传机制、化石和其他独立证据
美国国家医学图书馆的分子系统发育教材,明确强调基因树与物种树的区别,以及序列比对、树重建、可靠性评估和分子钟校准在研究流程中的不同职责。[NCBI Bookshelf]

所以,进化树究竟有多可信?

这需要根据具体的数据与模型判断。当许多独立的基因数据、化石证据以及其他信息,共同支持相同的分支关系时,我们通常可以获得更有说服力的推断。但如果某个内部节点的支持很弱。或者不同基因给出明显冲突的拓扑。科学上合理的做法,不是把冲突隐藏起来。而是进一步研究:这些差异源于统计噪声?模型不合适?序列比对错误?还是反映真正复杂的进化过程?一棵进化树最重要的价值,不是它看起来有多漂亮,而是它怎样将可观察的证据、可计算的模型和可检验的历史假设连接起来。

第十五幕:从达尔文的纸笔,到1987年的计算机——算法改变了什么?

现在,让我们重新回到1837年。那一年,达尔文在自己的笔记本上画下分支。树的结构,表达了一个极具革命性的想法。今天看起来非常不同的生物,可能拥有共同的演化来源。不同的物种,可以通过分叉与变化联系起来。不过,达尔文面对的是一个巨大的科学问题。当时没有DNA测序。没有分子替换模型。也没有能够自动搜索数百万种候选拓扑的计算机。因此,他笔记本上的那棵树首先是一种解释生物演化的思想图形。它帮助科学家思考:物种之间的关系,是否具有历史性的分支结构?到了20世纪,分子生物学改变了可以获取的证据。研究者开始拥有:蛋白质序列。DNA序列。越来越长的分子比对。以及成千上万个可以相互比较的基因。新的证据带来了新的问题。如果一个序列发生了多次替换,怎样估计实际演化距离?如果有很多不同树形,怎样选择值得研究的候选?如果不同谱系演化速度不一样,怎样避免错误聚类?如果一棵树只由少数DNA位置支持,怎样表达其不确定性?于是,系统发育重建从比较形态与绘制关系图,发展成了一项数学与统计计算任务。

三条经典研究路线

第一条:距离法。将序列差异转换成距离矩阵,再寻找能够合理解释这些距离的树。例如:UPGMA。Neighbor-Joining。第二条:最大简约法。寻找能够以尽可能少的演化变化解释观测状态的树。第三条:概率模型与最大似然。给定替换模型、树结构和分支长度,计算观察到当前序列的似然,再寻找较好的参数和树形。不同方法都有自己的优点与适用条件。不存在一条可以无条件替代全部方法的简单规则。

真正令人惊讶的,是算法思想的跨领域迁移

在第49章,维特比算法使用动态规划,恢复最可能的隐藏状态路径。第51章,Needleman–Wunsch和Smith–Waterman使用动态规划,寻找最优序列对应。第52章,BLAST借助启发式短词搜索,在巨大数据库中寻找相似线索。第53章,HMM使用Forward、Backward和Baum–Welch,对隐藏状态求和和估计参数。第54章,de Bruijn图与欧拉路径,将DNA碎片重新组织成图论问题。现在,第55章,Felsenstein的树似然算法又一次使用动态规划。而Neighbor-Joining则通过距离矩阵的逐步更新,高效构造无根进化树。表面上看:通信。序列比对。概率模型。图组装。系统发育树。是完全不同的问题。但计算机科学提供了一种可以反复迁移的方法论。首先找到问题中的结构。然后设计状态、评分、距离或概率模型。最后利用结构,减少不必要的计算,并明确结果成立的条件。这就是本篇五章真正希望传达的核心思想。

尾声:生命的家谱,不是由一条DNA自动讲完的故事

1837年。一位年轻的博物学家,在笔记本上画下一棵分叉的树。他试图用这棵树,理解不同物种之间的历史关系。在那个年代,生命的演化历史还没有被写成可以逐位比较的DNA序列。但树形结构所表达的思想,已经具有深刻意义。不同的生命可以拥有共同来源。不同的分支能够在时间中逐渐分化。而我们今天观察到的物种,只是漫长历史中仍然存在的一部分末端。一百多年之后,分子生物学让科学家获得了新的证据。蛋白质拥有可以测定的氨基酸序列。DNA拥有可以逐位比较的核苷酸序列。这些序列之间的差异,开始成为研究演化关系的重要线索。但新的证据并没有让问题自动变得简单。因为同一个DNA位置,可能发生过不止一次替换。不同的谱系,可能拥有不同的演化速度。一组有限的观测数据,可能同时与多种候选树相容。因此,研究者必须将生物学证据转化成数学模型。1965年,Zuckerkandl与Pauling讨论了分子如何保存进化历史。1967年,Fitch与Margoliash进一步研究怎样根据分子差异构造系统发育树。1981年,Felsenstein将DNA替换概率与树的最大似然计算联系起来。1985年,他又将Bootstrap重采样用于评估系统发育分支的支持度。1987年,Saitou与Nei提出Neighbor-Joining。它让研究者能够从距离矩阵出发,高效构造候选无根树,而不用首先穷举全部拓扑。从此,系统发育树的研究变得更加可计算,也更加依赖明确的模型假设。

一项与其他算法不同的科学问题

在前几章,我们经常能直接检查算法的正确性。例如:排序算法输出的数字是否有序。最短路径的总权重是否正确。一份DNA组装结果是否能够重新生成已知的k-mer集合。但系统发育树有时更加困难。因为我们试图恢复的对象,是早已发生、无法完整观察的历史。我们没有一台机器,可以直接返回:
text
这是五千万年前真正发生过的全部祖先分化。
科学家拥有的是:今天保存下来的序列。化石记录。形态特征。以及其他间接证据。算法只能根据这些数据和模型,推断过去。因此,一项成熟的系统发育分析,不仅应当输出一棵树。还应当说明:使用了哪些数据。采用了什么替换模型。怎样估计分支长度。树根依据是什么。哪些分支得到较强支持。以及哪些结论仍然存在不确定性。生命的演化历史,是科学家尝试从有限证据中恢复的真实历史;算法输出的树,则是关于这段历史的一项可检验推断。

本章最重要的五个数学表达式

第55章 · 数学核心回顾
1. 观察到的序列差异比例p=DL\boxed{p=\frac DL}

2. JC69核苷酸替换距离d=−34ln⁡(1−43p)\boxed{d=-\frac34\ln(1-\frac43p)}

3. Neighbor-Joining邻接判别量Q(i,j)=(n−2)d(i,j)−ri−rj\boxed{Q(i,j)=(n-2)d(i,j)-r_i-r_j}

4. 新内部节点的距离更新d(u,k)=d(i,k)+d(j,k)−d(i,j)2\boxed{ d(u,k)= \frac{d(i,k)+d(j,k)-d(i,j)}2 }

5. 给定树与模型的DNA对数似然ℓ(T,θ)=∑k=1Llog⁡P(Dk∣T,θ)\boxed{ \ell(T,\theta)= \sum_{k=1}^{L} \log P(D_k\mid T,\theta) }
它们代表三种不同层次的思考。第一:如何从序列得到可比较的数据?第二:怎样从距离推断分支结构?第三:怎样从概率模型出发,评估一棵候选树对观测数据的解释能力?而Bootstrap又进一步追问:当数据本身发生合理的抽样变化时,我们的分支结论还能够保持多么稳定?

第五篇正式完结:破解生命的密码

回顾第五篇的五章,我们经历了一次从分子实验到计算理论的完整旅程。
第二季 · 第五篇 · 第51—55章

破解生命的密码

51
寻找生命的相似之处:序列比对的诞生Needleman–Wunsch与Smith–Waterman,利用动态规划,寻找序列之间的最佳对应关系。

52
在浩瀚基因数据库里寻找一个线索:BLAST利用短词种子、启发式延伸和统计显著性,快速寻找高分局部相似片段。

53
看不见的生命状态:隐马尔可夫模型Forward、Backward、Viterbi与Baum–Welch,从观测序列推断隐藏结构并学习概率参数。

54
把破碎的生命之书重新拼起来:基因组组装将DNA读段转化为de Bruijn图,借助欧拉路径与其他约束恢复序列。

55
生命的家谱:算法怎样重建进化树通过进化距离、Neighbor-Joining、最大似然与Bootstrap,重建并评估分子演化关系。
这五章中,生物学问题不断发生变化。一开始,我们想知道:两条蛋白质序列是否相似?后来,我们想知道:巨大数据库里有哪些相似序列?再后来:如果DNA字符背后存在不可直接观察的区域状态,能否推断它们?接着:如果DNA本身被分成无数短片段,能否恢复原来的顺序?最后:如果我们已经拥有多个物种的序列,能否推断它们共同的演化历史?这些任务分别调用了:动态规划。启发式搜索。概率推断。图论。组合优化。以及统计重采样。于是,一个更加深刻的事实逐渐清晰。算法的力量不只是让计算机更快处理数据。它还帮助科学家把难以直接观察的问题,转化成可以形式化、计算和验证的研究对象。但是,算法不能脱离科学证据。序列比对的高分,不自动证明共同祖先。BLAST的低E-value,不自动证明相同功能。HMM的最大似然路径,不自动证明真实基因结构。组装器输出的最长Contig,不自动证明组装完全正确。而一棵获得较高Bootstrap支持的进化树,也不能被当作已经直接观察到的历史事实。因此,我们需要始终保留一项非常重要的科学态度。算法负责让证据变得可以计算;科学负责判断这些计算结果究竟意味着什么。生命并没有用人类能直接阅读的语言,写下一部完整的历史。它只是让分子一代代复制。让某些字符改变。让某些结构保留下来。也让大量信息在漫长时间中消失。今天,科学家只能面对那些仍然保存下来的痕迹。DNA中的一处替换。蛋白质中的一段保守序列。两个基因之间的共同片段。又或者,多个物种序列之间的一组统计关系。算法将这些零散线索组织起来。使它们逐渐构成一张可能的演化地图。但这张地图永远需要与新的证据相互检验。我们不是让计算机凭空创造了生命的家谱。而是让数学帮助我们,尽可能谨慎地读出生命留在分子里的历史。

历史档案:从1837年的分支草图到现代分子系统发育

年份研究者与成果历史意义
1837年达尔文在物种演变笔记本中绘制分支树共同祖先与分支演化思想的重要历史记录
1859年《物种起源》出版使用分支演化解释物种间的历史关系
1958年Sokal与Michener的数值分类研究UPGMA等层次聚类构树方法的重要来源
1965年Zuckerkandl与Pauling发表分子进化论文分子序列作为演化历史证据的经典研究
1967年1月20日Fitch与Margoliash发表构树研究利用分子差异估计系统发育关系
1969年Jukes与Cantor提出经典替换模型将多次核苷酸替换纳入概率距离修正
1981年Felsenstein发表DNA最大似然构树论文建立高效树似然计算与统计构树方法
1985年7月Felsenstein发表系统发育Bootstrap方法评估树分支对数据重采样的稳定性
1987年7月Saitou与Nei发表Neighbor-Joining发展高效的无根距离构树算法

主要参考文献与一手史料

1837年 · 达尔文的“I think”草图Darwin Correspondence Project — Darwin's Species Notebooks剑桥大学 — 达尔文原始笔记历史核查1837年草图的真实年代、档案来源,以及它在达尔文物种演变思想形成过程中的地位。

1965年 · 分子作为演化历史的文献Zuckerkandl & Pauling — Molecules as Documents of Evolutionary History《Journal of Theoretical Biology》第8卷第2期,第357—366页。

1967年 · Fitch–Margoliash树构造研究Fitch & Margoliash — Construction of Phylogenetic Trees《Science》第155卷第3760期,第279—284页。分子距离构树发展史的重要原始文献。

1969年 · Jukes–Cantor替换模型Jukes & Cantor — Evolution of Protein Molecules(历史论文)收录于《Mammalian Protein Metabolism》。本章使用的是后来系统发育分析中广泛采用的JC69核苷酸替换公式。

1981年 · Felsenstein最大似然原始论文Evolutionary Trees from DNA Sequences: A Maximum Likelihood Approach(原始论文PDF)2021年历史回顾:Felsenstein Phylogenetic Likelihood《Journal of Molecular Evolution》第17卷第6期,第368—376页。重点研究DNA树似然、分支长度估计与动态规划剪枝计算。

1985年 · Bootstrap评估系统发育树Felsenstein — Confidence Limits on Phylogenies: An Approach Using the Bootstrap《Evolution》第39卷第4期,第783—791页。用于核查有放回地重采样对齐位点、反复构树以及分支支持度的原始思路。

1987年 · Neighbor-Joining原始论文Saitou & Nei — The Neighbor-Joining Method(完整PDF)Molecular Biology and Evolution — 期刊正式书目《Molecular Biology and Evolution》第4卷第4期,第406—425页。本章最重要的一手资料之一,包含星形树、逐步选择邻居、分支长度估计以及计算机模拟比较。

分子系统发育的完整科学背景NCBI Bookshelf — Molecular Phylogenetics系统讨论序列比对、分子距离、树根、基因树与物种树、置信度评估及分子钟等问题。
**历史与教学重构说明:**1837年达尔文笔记、1965年分子进化研究、1967年Fitch–Margoliash论文、1981年Felsenstein最大似然研究、1985年Bootstrap论文,以及1987年Saitou–Nei Neighbor-Joining论文,均有对应的历史文献支持。本章的开篇叙事、人工物种A/B/C/D及其距离矩阵,属于教育性重构,而非某次真实的DNA实验现场记录。四物种距离矩阵是严格可加的教学数据;互动实验中的距离扰动,不是生物替换过程的模拟。Python的Neighbor-Joining实现只展示朴素距离构树与分支长度计算;最大似然程序展示固定模型和固定分支长度下的树似然递推,没有完成全部参数与拓扑的优化。Bootstrap实验展示统计重采样思想,其支持比例不可直接解释为候选树真实的后验概率。
下一篇 · 第六篇:会学习的算法

第56章:《沿着山坡寻找最低点:梯度下降的漫长历史》

1847年10月18日,法国巴黎。法国数学家奥古斯丁-路易·柯西(Augustin-Louis Cauchy)在科学院报告了一项数学研究。他想研究怎样求解复杂的方程组。对于某些问题,直接消去变量可能非常困难。于是,他考虑另一种路线。如果能够将问题转化成一个非负的函数,并逐步让这个函数的数值下降,是否有机会找到满足方程的解?他注意到一个重要事实。对于可微函数,梯度指出了局部增长最快的方向。如果沿着相反方向移动一小步,函数值通常就有机会下降。这项研究发表于1847年《Comptes Rendus de l'Académie des Sciences》,题为:Méthode générale pour la résolution des systèmes d'équations simultanées《求解联立方程组的一般方法》。后来,人们将这类思想发展成:Gradient Descent(梯度下降)。它的基本迭代形式可以写成:xt+1=xt−η∇f(xt)\boxed{ x_{t+1}=x_t-\eta\nabla f(x_t) }其中:η\eta是步长,也常被称为学习率。而:∇f(xt)\nabla f(x_t)是当前位置的梯度。但一项新的数学困难很快出现了。如果学习率太大,会不会在最优点两侧反复震荡?如果学习率太小,会不会下降得极其缓慢?如果函数包含多个局部极小值,又应该怎么办?更进一步:1847年的柯西,当然并没有现代神经网络的训练任务。但他研究的沿下降方向迭代更新方法,后来却成为机器学习优化中的重要基础。从19世纪的数值分析,到20世纪的优化理论,再到现代神经网络中的随机梯度下降,这项思想跨越了一个多世纪。 [滑铁卢大学计算机科学系][EMS Press]下一章,我们将正式进入第六篇《会学习的算法》。我们会从柯西1847年的原始问题开始,追踪最速下降法、步长选择和现代梯度下降的发展,并建立一张可以拖动初始点的二维损失函数地形图。你可以改变学习率。观察参数怎样一步步下降。也可以亲眼看到,为什么某些学习率导致收敛、某些导致震荡,还有一些会让算法直接发散。第五篇,我们利用算法寻找生命留下的历史线索;第六篇,我们将开始研究,机器怎样通过反复调整自身参数,从数据与错误中逐渐学习。
——《算法的故事》第二季 · 第五篇《破解生命的密码》 · 第55章 完

本章配图