自私基因的生物计算机模拟


TL;DR:我做了一个猴子行为进化的计算机模拟,继续阅读,看看这个问题最初是如何在 《自私的基因》 中陈述的。第一部分展示了我的 Java实现 ,第二部分展示了图表化的结果和结论。

问题

我最近读了 Richard Dawkins The Selfish Gene ,尽管是40年前的书,但非常令人大开眼界。虽然原文有时已经过时(" 你只能把几百个晶体管装进一个头骨 --现在更像是 万亿 ,你好 摩尔定律 )一般的主张和以前一样吸引人。

作为一名计算机工程师,有一章特别吸引了我的注意,是关于动物进行 s 特殊的梳理 ,比如某些种类的猴子(见上图)。让我引用 相关章节

假设一个物种[......]被一种特别讨厌的 蜱虫 寄生,它携带一种危险的疾病。应尽快清除这些蜱虫,这是非常重要的。[......]一个人可能无法够到自己的头,但没有什么比一个朋友为他做这件事更容易了。后来,当这位朋友自己被寄生时,这个善举就可以得到回报了。[......]这有直接的直觉意义。任何有意识的预见性的人都可以看到,达成相互的背骗安排是明智的。[...]

假设B的头顶上有一个寄生虫。A把它从他身上拉下来。后来,当A的头上也有寄生虫的时候。他自然而然地找上了B,以便B能报答他的善举。B只是抬起鼻子,走了过去。B是一个骗子,一个接受了其他个人的利他主义的利益,但却没有偿还,或者偿还得不够的人。骗子比不分青红皂白的利他主义者做得更好,因为他们获得了利益而没有付出代价。可以肯定的是,与清除危险的寄生虫的好处相比,梳理另一个人的头的成本似乎很小,但它并不是可以忽略不计的。必须花费一些宝贵的精力和时间。

让种群由采取两种策略之一的个体组成。[......]称这两种策略为吸食者和欺骗者。吸血者不分青红皂白地为任何需要帮助的人提供服务。骗子接受吸血者的利他主义,但他们从不为其他人做嫁衣,甚至是之前为他们做嫁衣的人。[......]作弊者会比吸血者做得更好。即使整个人口下降到灭绝,也不会有任何时候吸血者比骗子做得更好。因此,只要我们只考虑这两种策略,就没有什么能阻止吸血者的灭绝,而且很可能也阻止整个人口的灭绝。

但是现在,假设有第三种策略,叫做Grudger。怨恨者对陌生人和以前曾对他们进行过诱导的个人进行诱导。然而,如果有任何个体欺骗了他们,他们就会记住这件事并怀恨在心:他们拒绝在未来为该个体做美容。在一个由记仇者和吸血者组成的群体中,不可能分辨出哪个是谁。两种类型的人都对其他人表现出利他主义[......]。如果怨恨者与欺骗者相比很少,怨恨者的基因就会灭绝。然而,一旦怨恨者设法增加数量,使其达到一个临界比例,他们彼此相遇的机会就会变得足够大,足以抵消他们在培养骗子方面的浪费。当达到这个临界比例时,他们将开始比作弊者有更高的平均报酬,而作弊者将以加速的速度被赶向灭亡。[...]

引自: 《自私的基因》 ,作者: 理查德-道金斯 ,书号:0-19-857519-X。

后来作者进行了一系列的计算机模拟,观察这三种策略在不同条件下如何共同发挥作用。很明显,源代码是不可用的,其实我很高兴。首先是因为我有机会写一些代码来玩。其次:这本书是1976年出版的,在C语言发明4年后,比C++早很多年(说句题外话),而且我也没有心情去看(我想) Fortran

实现

所以我黑了几个类,对整个人口中的作弊者、吸血者和怨恨者进行模拟。我想看看不同类型的行为如何影响种群大小。哪种行为是稳定的并保证生存。最后是如何缓解随机突变和死亡的。我决定试验一些技术,即。

  1. 手工制作的依赖注入,没有任何容器
  2. 完全是单线程
  3. 明确推进的逻辑时钟,模拟时间流逝

我们要模拟一群猴子(从几只到几百万只),每只猴子都有独立的行为,随机的生活时间等等。使用像行动者或至少是线程这样的多代理解决方案似乎很明显。然而 《反应式编程原理》 课程告诉我,这往往是过度的工程化。本质上,这样的模拟是一连串未来应该发生的事件:一只猴子应该在2年内出生,应该在5年内繁殖,应该在10年内死亡。当然,这样的事件还有很多,也有更多的猴子。然而,把所有这些事件扔进一个优先级队列就足够了,在这个队列中,未来最接近的事件排在第一位。这就是 Planner 的基本实现方式。


public abstract class Action implements Comparable<Action> {

    private final Instant schedule;

    public Action(Clock simulationTime, Duration delay) {
        this.schedule = simulationTime.instant().plus(delay);
    }

    @Override
    public int compareTo(Action other) {
        return this.schedule.compareTo(other.schedule);
    }

    public abstract void run();

}

//...

public class Planner implements Runnable {

    private final Queue<Action> pending = new PriorityQueue<>();
    private final SimulationClock simulationClock;

    public void schedule(Action action) {
        pending.add(action);
    }

    @Override
    public void run() {
        while (!pending.isEmpty()) {
            Action nearestAction = pending.poll();
            simulationClock.advanceTo(nearestAction.getSchedule());
            nearestAction.run();
        }
    }
}


我们有一个 Action 的预定义

schedule

( 何时 应该被执行)和一个

pending

未来行动的队列。不需要等待,我们只需从未来行动中挑选最近的行动,并推进模拟时间。


import java.time.Clock;

public class SimulationClock extends Clock {

    private Instant simulationNow = Instant.now();

    @Override
    public Instant instant() {
        return simulationNow;
    }

    public void advanceTo(Instant instant) {
        simulationNow = instant;
    }

}


所以我们基本上实现了一个 事件循环 ,事件可以被添加到队列中的任何地方(不一定是最后)。现在我们有了一个基本框架,让我们来实现猴子的行为。


public class Sucker extends Monkey {
    //...

    @Override
    public boolean acceptsToGroom(Monkey monkey) {
        return true;
    }

}

public class Cheater extends Monkey {

    private final double acceptProbability;

    //...

    @Override
    public boolean acceptsToGroom(Monkey monkey) {
        return Math.random() < acceptProbability;
    }
}

public class Grudger extends Monkey {

    private final Set<Monkey> cheaters = new HashSet<>();

    @Override
    public boolean acceptsToGroom(Monkey monkey) {
        return !cheaters.contains(monkey);
    }

    @Override
    public void monkeyRejectedToGroomMe(Monkey monkey) {
        cheaters.add(monkey);
    }

}


你可以看到这三个类捕获了三种不同的行为。

Sucker

s 总是接受梳理请求,

Cheater

s 只是有时(在最初的模拟中--从不,但我让这一点可以配置),

Grudger

s 记得之前谁拒绝了他们的请求。猴子被聚集在一个类中

Population

,这里有一个小片段。


public class Population {

    private final Set<Monkey> monkeys = new HashSet<>();
    private final MonkeyFactory monkeyFactory;

    private Population addMonkey(Monkey child) {
        if (!full()) {
            newMonkey(child);
        }
        return this;
    }

    private boolean full() {
        return monkeys.size() >= environment.getMaxPopulationSize();
    }

    private void newMonkey(Monkey child) {
        monkeys.add(child);
        planner.scheduleMonkeyLifecycle(child, this);
        log.debug("New monkey in population {}total {}", child, monkeys.size());
    }

//...
}


对于每只新猴子,我们安排其所谓的生命周期,即与繁殖、梳理和死亡有关的事件(在

Planner

):


void scheduleMonkeyLifecycle(Monkey child, Population population) {
    askForGrooming(child, environment.getParasiteInfection().make(), population);
    scheduleBreedings(child, population);
    kill(child, environment.getLifetime().make(), population);
}

void askForGrooming(Monkey child, Duration parasiteInfection, Population population) {
    schedule(new AskForGrooming(child, parasiteInfection, population));
}

private void scheduleBreedings(Monkey child, Population population) {
    final int childrenCount = RANDOM.nextInt(environment.getMaxChildren() + 1);
    IntStream.
            rangeClosed(1, childrenCount)
            .forEach(x -> breed(child, environment.getBreeding().make(), population));
}

void kill(Monkey child, Duration lifetime, Population population) {
    schedule(new Kill(child, lifetime, population));
}

private void breed(Monkey child, Duration breeding, Population population) {
    schedule(new Breed(child, breeding, population));
}


AskForGrooming , Kill , Breed 等是已经提到的

的实例。 Action

类的实例,例如:

Kill

:


public class Kill extends MonkeyAction {
    private final Population population;

    public Kill(Monkey monkey, Duration lifetime, Population population) {
        super(monkey, lifetime);
        this.population = population;
    }

    @Override
    public void run(Monkey monkey) {
        population.kill(monkey);
    }
}


我将所有的模拟参数封装在一个简单的值类中 Environment ,许多参数如

parasiteInfection

,

lifetime

breeding

不是常数,而是 RandomPeriod 类的实例。


@Value
public class RandomPeriod {

    private static final Random RANDOM = new Random();

    Period expected;
    Period stdDev;

    public Duration make() {
        final long shift = Periods.toDuration(expected).toMillis();
        final long stdDev = Periods.toDuration(this.stdDev).toMillis();
        final double gaussian = RANDOM.nextGaussian() * stdDev;
        double randomMillis = shift + gaussian;
        return Duration.ofMillis((long) randomMillis);
    }

}


这使我能够捕捉到具有期望值、标准差和 正态分布的随机时间段的概念

make()

方法只是生成了一个这样的随机期。我不打算探索这个模拟的全部源代码,它 可以在GitHub 上找到。现在终于到了运行几次并观察种群如何增长(或灭绝)的时候了。顺便说一下,我使用相同的计划器和行动机制来窥探发生了什么。我只需每年注入一次 Probe 行动(逻辑时间!),并输出当前的人口数量。

就像许多事件循环一样,必须只有一个线程访问事件。我们遵循这种做法,模拟是单线程的,因此根本不需要执行任何同步或锁定,我们也可以使用标准的、不安全的但更快的集合。更少的上下文切换和改进的缓存定位也有帮助。此外,我们还可以很容易地将模拟状态转储到磁盘上,例如,以后可以恢复它。当然,也有缺点。在有成千上万只猴子的情况下,模拟速度会变慢,除了仔细优化和购买更快的CPU(甚至是更多的CPU!),我们能做的不多

实验

作为对照组,我们从一个仅由吸食者和吸食者与怨恨者混合组成的微小(10个标本)群体开始。在没有欺骗者的情况下,这两种行为是无法区分的。我们关闭突变(吸血者和怨恨者的孩子有可能成为欺骗者,而不是吸血者或怨恨者),看看种群如何增长(X轴代表时间,Y轴是种群大小)。

注意吸血者和怨恨者的比例在50%左右波动,因为这两种行为做的是完全相同的事情。只用几个作弊者进行模拟是没有意义的。因为他们一般不会互相疏导,他们很快就会死亡,抹去" 作弊基因 "。另一方面,只有吸血者(没有突变)在成倍增长(你可以清楚地看到新的一代在高原后诞生)。

然而,如果我们模拟一个有100个吸血者和5个作弊者的种群会发生什么?突变再次被关闭,以保持模拟的清洁。

有两种可能的情况:要么作弊基因消失,要么它扩散并导致人口灭绝。这是一种悖论--如果这个特定的基因赢了,整个种群(包括这个基因!)都会被毁灭。现在让我们来模拟一些更有趣的事情。我们打开5%的突变概率,要求作弊者在10个案例中的9个案例中进行新郎(所以他们的行为有点像吸血者)。我们从一个由5个吸血者和5个记仇者组成的健康群体开始。

你看到怨恨者是如何迅速扩大并几乎总是超过吸血者的吗?这是因为在一个有时因突变而出现作弊者的环境中,吸血者更容易受到伤害。你是否注意到,每次出现哪怕是少量的吸血者,种群都会迅速萎缩?怨恨者也很脆弱:他们培养新生的作弊者,却还不知道他们是谁。但是他们不会像吸血者那样重复这种错误。这就是为什么吸血者总是松动,但他们并没有完全灭绝,因为他们在某种程度上受到了怨恨者的保护。不是直接的,但是怨恨者通过不梳理骗子来杀死他们,减少威胁。这就是不同行为的合作方式。那么,为什么这个种群终究还是灭绝了呢?仔细看图表的末尾,在某个时间点,由于一个随机的原因,吸食者的数量超过了怨恨者--这在当时没有作弊者的情况下尤其可能。那么发生了什么?由于突变,突然出现了几个作弊者,这个猴子的社会就注定了。

现在我们来研究另一个例子:一个已经有100个吸血者生活的成熟社会,没有观察到其他行为。当然,由于突变,怨恨者和欺骗者很快就出现了。大多数情况下,这种模拟很快就结束了,只是在几代之后。欺骗者出生的概率与怨恨者出生的概率相同,但我们需要更多的怨恨者来保护吸血者。因此,人口会死亡。但我设法进行了一些模拟,在这些模拟中,这样的社会实际上生存了一段时间。

有趣的是,吸血者在人口中占了相当长的时间,但另一次 流行病 欺骗者的攻击杀死了大部分吸血者。不幸的是,作弊者的最后一次增长使怨恨者的数量减少到他们不能再保护吸血者的地步,突然间所有人都死了。

总结

从实施的角度来看,使用单线程模型和行动队列的效果非常好,尽管很难扩展。然而,生物学上的结论要有趣得多。

  • 只要没有作弊者,一个利他主义的人口( s 只有uckers )可以永远快乐地生活。试图利用这个系统
  • 在一个利他主义的种群中出现一个作弊者会导致这个种群崩溃
  • 种群需要警卫 ( 怨恨者 ) 来保护免受作弊者的伤害
  • 即使在警卫的存在下,仍然有少量的作弊者不被注意
  • 如果作弊者的数量超过某个关键比例,种群不能再保护自己而放弃了。每个标本都会死亡,包括作弊者
  • 通常对种群不利的基因会灭绝,即使这拖累了整个种群

你可以自由地将结论扩展到人类社会。 完整的源代码 可以在GitHub上找到,请自由地进行实验,也欢迎拉动请求!