最近我正在看一本书,叫做 Cells To Civilizations(《从细胞到文明》)。这本书写得清晰明了,很开眼界。第一部分阐述「自然选择」的原理,让我对这个问题有了新的理解。由此我受到启发,写了一个 JavaScript 脚本,来模拟「物竞天择,适者生存」的系统。和理论分析相比,这段代码用数据一步步直观展示了演化是怎样进行的。

物竞

「物竞」与「天择」是两个不同的过程,下面分别讨论,先看「物竞」有哪些特点。

物竞是生物为争夺资源而竞争(竞争只是一种形象说法,不是说动物们在有意识的争斗),这是十分必要的,因为任何体系都不是无限大的,竞争是系统自我保护的一种自然机制。

现在设想某个池塘中生活两种鱼,一种黑色,一种白色,并满足如下条件:

  1. 黑鱼生黑鱼,白鱼生白鱼,且繁殖速度相同(假设生殖速度是线性的,且不考虑基因变异等情况)。
  2. 每当鱼的数量达到 1000 时,就触发「竞争」,杀死鱼群的 90%。不论黑白,生存机会平等。也就是说,1000 条鱼的每一条都有同样的机会竞争那 100 个名额。

假设初始状态,池塘有 500 条黑鱼 500 条白鱼。经过 100 次竞争,你认为会发生什么?

根据条件,初始有 1000 条鱼,会发生竞争,留下 100 条。由于存活概率相同,我们猜测大概是 50 黑 50 白。接着这 100 条鱼一代代繁殖,变成第 2 代 200 条,第 3 代 300 条……直到 1000 条,再次发生竞争,如此反复循环,每 10 代到达 1000,那么 100 次竞争相当于繁殖 1000 代。因此第一反应可能是,黑鱼白鱼机会完全平等,看起来它们的数目会大致相同。

然而实际情况并非如此。

我写了一段 JavaScript(源代码见末尾)来模拟这个系统。只要设置参数 N,表示竞争次数即可,N 默认 100。

先用 N = 100,然后运行,结果如下图,黑线代表黑鱼,红线代表白鱼(为了显示清楚):

图 1 Chart 1

可见,不是势均力敌,而是赢者通吃,黑鱼:白鱼 = 1000:0,白鱼灭绝。

多试几次看看怎么样,下图是连续运行 5 次的结果:

图 2 Chart 2

结果分别为:

  1. 黑:白 = 1000:0
  2. 黑:白 = 1000:0
  3. 黑:白 = 0:1000
  4. 黑:白 = 930:70
  5. 黑:白 = 610:390

前四次趋势很明显,最后一次差别不大,应该是演化时间不够长,如果增加 N(记住 N 增加 1 意味着竞争 1 次,即繁殖多 10 代),效果应该更明显。

现在设置 N = 200,依然运行 5 次:

图 3 Chart3

200 次竞争,结果看起来稳定多了,分别是:

  1. 黑:白 = 0:1000
  2. 黑:白 = 940:60
  3. 黑:白 = 1000:0
  4. 黑:白 = 0:1000
  5. 黑:白 = 0:1000

可见「竞争」+「稳定遗传」这两个机制会造成一面倒的结果。怎样解释这个现象呢?

这是因为如果我们从 500 黑鱼 500 白鱼的系统中随机选出 100 条,结果并不一定是 50 黑,50 白,而有可能是比如 52 黑,48 白。稳定遗传意味着,概率的微小扰动经过 10 代繁衍会被放大,此时黑鱼是 520 条,白鱼 480 条。发生竞争时,这个系统更「偏向」黑鱼,因为它的比例大。这是一个正反馈系统,黑鱼越多,生存的概率越大,进一步导致活下来的黑鱼更多,直到池塘全都是黑鱼。

有意思的是,如果仔细观察上图,虽然最终结果要么全黑,要么全白,这个过程却不是一帆风顺的。例如这个情况:

图 4 chart 4

黑:白一度超过 800:200,却被白鱼逆转。

现在我们可以总结竞争的特点:

  1. 这是一个负反馈系统(鱼群数量的变化是正反馈,不要搞混),即鱼群数量增加造成资源不足,资源不足反过来造成鱼群数量锐减,回到初始状态,如此循环。
  2. 尽管「机会均等」,结果却是赢者通吃,这个过程是正反馈。
  3. 劣势一方(如白鱼)会灭绝(这一点看下文会更清楚)。
  4. 虽然结果总是全黑或全白,事先却无法预知究竟是哪一个。它像「薛定谔的猫」,只有观察的时候才知道。而且领先的未必会赢到最后。

最后一点揭示了「适者生存」这个概念的关键:「适者」是指那些生存下来的吗?如果是,那么对于图 3 中五种情况,我们只能说,适者有时是黑鱼,有时是白鱼,这是很荒谬的。实际上,二者的机会是完全均等的,根本无法确定。

那么「适者生存」又是什么意思呢?解释这个概念仅仅存在竞争是不够的,需要引入更多的变量。上面的例子所揭示的,只是演化(Evolution),不包括自然选择机制(Natural Selection),有了对「物竞」的理解,现在我们来看看加上「天择」会怎样。

天择

假设遗传过程发生变异,例如,黑鱼繁殖速度比白鱼快 10 %,结果如何呢?

修改 JavaScript 脚本,引入一个新参数 rate,代表繁殖速度,经过 10 代之后:

黑鱼数量 = 黑鱼 * 10 * rate;
白鱼数量 = 白鱼 * 10

如果 rate = 1,那么相当于没有变异。现在设置 rate = 1.1,其余条件保持不变,还是 N = 100,运行 5 次,结果如下图:

图 5 chart 5

图 5 比图 1-4 收敛的更快,而且白鱼一次机会都没有。

我们还可以假设,黑鱼不是繁殖速度变强了,而是生存能力变强了,例如更抗饿,会怎么样呢?

再引入一个新参数 favor。favor = 1,相当于生存概率增加 1%,比如原来是 50%,现在则为 51%。因此 favor 越高,环境越偏爱黑鱼,存活几率就大。

为了更好地理解 favor 的含义,我简单说明一下这段 code,首先生成 1-100 之内的随机数,它表示鱼们竞争的那 100 个机会。

result = Math.floor( Math.random() * 100 ) + 1;

PB 表示黑鱼的归一化概率,假如有 515 条黑鱼,生存概率就是 51.5%,四舍五入,为 52%。

let PB = Math.round(x/10);

PB + favor 则表示在黑鱼原来的存活概率基础之上再,给它一个「turbo」,现在,我们用 result 和 PB + favor 比较,PB + favor 越大,result 落在这个区间的概率越高,表示黑鱼希望越大,因此 favor 相当于黑鱼的基因变异,生存能力变强了。

if (result <= (PB+favor)) {
	numOfBlack++;
} else {
	numOfWhite++;
}

favor 默认是 0,表示不偏不倚,也就是图 1-4 的情况。现在设置 favor = 10,把 rate 调回 1,依然 N = 100,运行 5 次,结果如下图:

图 6 Chart 6

可见结果对白鱼来说来说是毁灭性的,看来 10% 太大了,试试 1%:

图 7 chart 7

白鱼情况略好,但依然迅速灭绝了。说明结果对 favor 这个参数更加敏感,仅仅 1% 的变化就带来了明显的效果。

总之,图 1-4 我们看到,在所有条件平等的情况下,概率上的微小扰动被演化的正反馈机制放大,使某一方占据优势,直到某个时刻,不再可逆,最终出现一边倒的结果。但由于随机性,这个结果哪方占优是不可预测的。

而图 5-7说明,如果某一方的基因变得更「适应」环境( favorable ),那么它在进化中一定胜出,等待对方的只有灭绝,这就是「自然选择」。所谓的「适者」就是更优秀基因的携带者。还要说明,无论黑鱼基因多强大,只有竞争机制才会使白鱼灭绝,否则白鱼的比例会越来越小,但永远不会消失。

在千变万化的自然界,「适应」条件从来都不是固定的。上面的例子中,黑鱼因为身体强健而提高了生存能力,它也可能因此变得味道鲜美而更容易成为鸟类的美食。

一个实际的例子是,野生的小麦成熟时,麦穗自动爆开,落到地上,这样才能播种。有些小麦发生了变异,麦穗不会爆开,本来这意味着绝种。然而,恰恰是这样的小麦 10000 多年前被人类偶尔发现,进行驯化,成了人类主食,如今种遍全世界,绝对的基因劣势神奇反转,成为了绝对的优势,这里「适应」的含义就是人能吃。

最后再次说明,「物竞天择,适者生存」,既不是有意识的竞争,也不是有意识的选择,它只是一个冰冷的数学机制。

JavaScript code

//Competition parameter: number of competition cycles
let N = 100; 

/*
Natural selection parameters
favor = 1 means 1% more survival-favorable
rate = 1.1 means 10% higher proliferation rate of black fish over white fish, 
*/

let favor = 0; 
let rate = 1; 

let black = 500; // initial number of black fish
let white = 500; // initial number of white fish

let NB = ["500"];
let NW = ["500"];

for ( let i = 0; i < N; i++ ) {
		// competition occurs!	
		let num = simCompetition(black, favor); 

		//replicate 10 generations
		black = num[0] * 10 * rate; 
		white = num[1] * 10;		
		
		let normalized = normalize(black, white); // normalization over 1000
		black = normalized[0];
		white = normalized[1];
	
		// save variations
		NB.push(black); 
		NW.push(white);	
} // for i

console.log("Number of competition cycle: " + N+"(" + N * 10 + " generations)\n" )
console.log("Number of black fish: " + black);
console.log("Nubmer of white fish: " + white);
console.log("\n");

// copy arrays for plotting chart
let splitBlack = NB.toString().split(",").join("\n");
let splitWhite = NW.toString().split(",").join("\n");
Pasteboard.copy(splitBlack + "\n"+splitWhite);

// only argument x(number of black fish) needed here because the rest is for the white.
function simCompetition(x, favor) {
	let numOfBlack = 0;
	let numOfWhite = 0;
	let PB = Math.round(x/10); //rounded number, i.e. if x = 515, then PB is 52, meaning 52% probability

	// console.log("PB is " + PB);
	let result;
	for (let i = 1; i <= 100; i++) { // 100 survival chances
		 result = Math.floor( Math.random() * 100 ) + 1;

		if (result <= (PB+favor)) {
			numOfBlack++;
		} else {
			numOfWhite++;
		}
	} // end for()

	return [ numOfBlack, numOfWhite ];
	
} //function simCompetition

// this block will only be triggered when rate != 1
function normalize( x, y ) {
	if ( ( x + y ) >= 1000 ) {
		let norm = ( x + y ) / 1000; // normalization, taking "birth rate" into account
		x = Math.round( x / norm );
		y = Math.round( y / norm );
	} // if (( black + white ) >= 1000)

	return [x, y];
}