📘 第3章 自动机和人工生命程序
📄 一句话概括

1970 年,康威在《科学美国人》的数学游戏专栏里登了一个游戏。规则两句话: 活着的格子邻居太少或太多就死,空着的格子邻居正好三个就活。 半个世纪过去,这套规则还在被用来演示同一件事:复杂可以从极简里长出来。 这一节跑两个模型。 生命游戏看空间:几百万个格子同时按同一条规则变。兰顿蚂蚁看时间:一只蚂蚁按两条规则走, 第一万步之后突然开始修一条路。两个模型加起来不到十行代码,可都得跑出来才知道结果。

📝 本节和前面几节的关系

上一章的规则系统,规则是人写的,结论是人推的。 这一章换了个问法:如果规则简单到不用写,让它在时间上自己跑,会跑出什么? 生命游戏里的每个格子就是一个只有死活两态的自动机,整盘是四千多个这样的自动机并排跑。 下一节把自动机缩小到一个,看清它内部那张表。

一、人工生命问的不是「像不像人」

人工智能这个词容易让人以为目标是造一个人。 人工生命(artificial life)问的是另一个问题:生命这件事,能不能换一种物质来实现? 把「活着」拆成几条能写下来的性质:能自我复制、能对外界刺激作出反应、会在环境里留下痕迹、 能维持自己不被冲散。这几条没有一条要求碳和水。 冯·诺依曼在 1940 年代就问过「机器能不能造出跟自己一样的机器」。 他的答案是可以:一台「构造器」加一份「蓝图」,蓝图里也包括怎么复制蓝图。 这个结构后来被叫作自复制自动机,是人工生命的理论起点。 1987 年,兰顿在洛斯阿拉莫斯组织了第一次人工生命会议。 从那以后,「人工生命」指的是一个明确的研究纲领:不模拟某个具体生物, 只模拟「生命这一类过程」。 AI 关心「像不像人那样解题」,人工生命关心「复杂从哪来」。两边的人后来走到了一起: 今天讲大模型的「涌现能力」,用的就是这一套语言。

二、生命游戏:两条规则,五百一十二种邻域

康威的规则原话有四条,其实是两句话的正反面:

情形 下一代
活格,邻居 0 或 1 个 死(太孤单)
活格,邻居 2 或 3 个 活
活格,邻居 4 个以上 死(太挤)
死格,邻居正好 3 个 活(繁殖)
写成一行就是 B3/S23:B 是 birth(出生),S 是 survival(存活),后面的数字是邻居个数。
整个规则表就这么大。
一个格子加上周围八个,一共九格,每格死活两种,所以邻域一共 2 的 9 次方 = 512 种。
Demo 第一段把这 512 种逐个过了一遍:140 种下一代是活格,占 27.3%。其中 56 种是「死格变活」,
84 种是「活格继续活」,剩下 372 种下一代都是死格。
这个比例值得记一下:规则真正能起作用的面,只占四分之一多一点。大部分邻域,
下一代都是空的 —— 也就是说,大部分地方什么也不会发生。
关键在「局部」两个字。一个格子的命运只由它自己和八个邻居决定,它看不见棋盘另一头。
棋盘上没有任何一个地方在做总指挥。

三、跑出什么结局,规则里没写

Demo 第二段撒了八种密度,各跑 500 代,结局分了三类:

初始密度 500 代后 结局
0.02 0 格 全死光
0.05、0.30、0.40 20、115、120 格 进入周期
0.10、0.20 255、171 格 还在变
0.50、0.60 165、166 格 还在变
这几类结局没有一条规则写着。规则只说了「这一格下一代死活」,至于整盘会死光、会冻在原地、
还是会一直动下去,得跑出来才知道。
物理里管这个叫「相变」:参数(这里是密度)走到某个区间,系统的整体行为就换了一种模式。
生命游戏被研究得最透的,正是这张相图。
顺带说三个有名字的结构。静物是压根不动的图案,比如四格方块。
振荡子在两三个图案之间循环,比如三格的「闪光灯」周期 2、更大的「脉冲星」周期 3。
滑翔机最有名:五格的图案,四代之后整体斜着挪一格,形状不变。
滑翔机是生命游戏能当计算机用的关键。它是一颗能定向移动的信号;
两颗滑翔机对撞,可以当成「与」「或」「非」来用。2000 年前后,有人真在生命游戏里搭出了通用图灵机。
两条规则,足够算任何可算的东西。

四、改一个格子,两百代后差三百四十格

Demo 第三段做了个实验:拿一张跑到 300 代的棋盘,从活着的格子随便挑 20 个,每次翻一个, 再跑 200 代,跟没翻的那份对照。 结果分成两半。8 次改完等于没改,差值一直是 0; 剩下 12 次,第 200 代的差异从 17 格到 340 格(满盘 4096 格)。 「等于没改」的那几次不是程序错了。 翻的那一格本来就是孤零零的,边上没有邻居,按规则两代之内自己就死了 —— 死了就等于没翻过。 这说明敏感不是处处敏感。 翻在正热闹的地方就不一样。 有一格到第 20 代只差 69 格,第 200 代差到 340 格,整整一成的棋盘跟没翻的那份不一样了。 规则里没有一个字提到「传播」,差异却自己散开了。 这带来一个很实际的后果:这套规则写得出、算得快,但预测不了。想知道第 500 代长什么样, 只能一代一代跑过去。 Demo 还补了一组更直白的证据: 八个随机种子,同样的密度 0.30、同样的规则,500 代后的存活数是 355、117、184、215、386、138、 232、178,最多和最少差 3.3 倍。规则完全一样,只是开局那一把随机撒得不一样。 所以人工生命的论文很少写「第 500 代一定是多少格」, 只写「大概落在哪个区间」「有没有进入循环」。这不是偷懒,是这类系统只允许统计性的结论。

五、兰顿蚂蚁:两条规则,第一万步开始修路

1986 年,兰顿提出一个比生命游戏还简单的模型。一张白纸,一只蚂蚁,两条规则:

脚下白格:右转 90 度,涂黑,前进一格
脚下黑格:左转 90 度,涂白,前进一格

除了这两条,什么也没有。纸是空白的,没有藏任何图案; 蚂蚁也没有记忆,它每次只读自己脚下这一格的颜色。 前一万步,蚂蚁在原地附近乱走。 Demo 量了它走过的范围:离出发点最远只到过 29 格。轨迹看着毫无章法,跟随机游走差不多。 第 9976 步之后,它开始重复。拿当前位置和 104 步之前比,差值固定在 (2, -2)。 每 104 步往右上挪两格,一格不差,一直挪到三万步。 这条重复的轨迹被叫作「高速公路」。 它不在任何一条规则里,也不在任何一处初始条件里。是两条规则跑了将近一万步之后自己撞出来的。 速度慢得离谱:平均每步 0.0275 格,一百万步才走两万七千格。但方向定死了,不再回头。 黑格数的涨法也跟着变:前 11000 步每千步多出 75.8 个,之后每千步多出 115.4 个。修路的同时, 它留下的黑格更密了。 这个结果后来被证明不是巧合: 从空白纸出发,任何一只兰顿蚂蚁最终都会修出高速公路,只是起点步数略有不同。 一个「一定会发生」的结论,从一个「看起来完全随机」的过程里长出来 —— 这是人工生命最想说明的事。

六、这套东西跟人工智能的关系

三个连接点,从近到远。 第一,元胞自动机是最早的并行计算模型。每个格子同时算自己的,没有中央处理器,也没有先后顺序。 今天的 GPU 上跑的东西思路是一样的:同一条指令,几万个单元一起执行。 第二,神经网络是同一个套路换了材料。都是「大量简单单元 + 局部连接 + 反复迭代」。 区别只有一处:生命游戏的规则是人写死的,神经网络的连接权重是从数据里学出来的。

模型 单元 规则从哪来 跑多久
生命游戏 格子(死活) 人写死 几百代
兰顿蚂蚁 一只蚂蚁 人写死 几万步
神经网络 神经元(数值) 从数据学 几万到几百万步
第三,「涌现」这个词是从这里借出去的。大模型规模上去之后突然会做算术、会翻译,没人写规则教它。
这个现象跟兰顿蚂蚁的高速公路是一类:规则简单、规模够大、时间够长,行为会跳到一个新档位。
这个词用得对不对,现在还有争论。但它至少提醒一件事:想知道系统会干什么,光读规则没用,得跑。

七、动手 Demo:生命游戏的两条规则,和兰顿蚂蚁的第一万步

这段代码里有三个东西:生命游戏的下一代、两种写法的速度差、兰顿蚂蚁的三万步。 生命游戏那部分只用了 numpy 的整块运算 —— 把棋盘往上、下、左、右各错一位再相加, 就得到每个格子的邻居数。这一步是整个 Demo 里唯一需要动脑的地方。 蚂蚁那部分用了一个集合记黑格,纸不设边界,所以不用管越界。

def neighbours(g):
    """八邻居求和,边界环绕。"""
    s = np.zeros_like(g, dtype=np.int16)
    for dr in (-1, 0, 1):
        for dc in (-1, 0, 1):
            if dr or dc:
                s += np.roll(np.roll(g, dr, 0), dc, 1)
    return s

def gen(g):
    """整个生命游戏,规则只有这一行。"""
    n = neighbours(g)
    return ((n == 3) | ((g == 1) & (n == 2))).astype(np.int8)

# 兰顿蚂蚁:纸不设边界,黑格记在集合里
black = set()
r = c = 0
d = 0
for i in range(1, 30001):
    if (r, c) in black:
        d = (d - 1) % 4          # 黑格:左转
        black.discard((r, c))    # 涂白
    else:
        d = (d + 1) % 4          # 白格:右转
        black.add((r, c))        # 涂黑
    r += (-1, 0, 1, 0)[d]
    c += (0, 1, 0, -1)[d]

实跑输出:

--------------------------------------------------------------------
两条规则的世界
--------------------------------------------------------------------
==============================================================

[1] 生命游戏:规则表只有两行

  一个格子加上周围八格,一共 9 个格,每个非死即活,
  所以一共 2 的 9 次方 = 512 种邻域。逐个数一遍:

    死格且邻居正好 3 个  →  下一代活(出生)   56 种
    活格且邻居 2 或 3 个  →  下一代活(延续)   84 种
    其余                  →  下一代死          372 种

  三行加起来 140 种,占 27.3%。规则表就这么大,背得下来。
  换句话说:一个格子的命运,只由它自己和八个邻居决定,
  它看不见棋盘另一头发生了什么。这是【局部】两个字的意思。

[2] 棋盘上撒一把随机格子,看最后剩下什么

  64x64 棋盘,边界环绕(上边和上边接、左边和左边接),跑 500 代。

    初始密度  存活数  周期  结局
    ----------------------------------------
    0.02      0       1     全死光
    0.05      20      2     原地打转
    0.10      255     无    还在变
    0.20      171     无    还在变
    0.30      115     2     原地打转
    0.40      120     2     原地打转
    0.50      165     无    还在变
    0.60      166     无    还在变

  撒得太稀(0.02)三个回合就死绝了;撒得密反而是小块在打转。
  中间那几档才有持续变化。这三种结局没有一条规则写着,
  它们是规则跑了 500 代之后自己长出来的。

[3] 改一个格子,两百代后差多少

  取一张 p=0.30 跑了 300 代的棋盘,从活着的地方随便挑 20 个格子,
  每次挑一个翻个面,再同样的规则跑 200 代,跟没翻的那份对照:

    翻哪一格      第 20 代  第 100 代 第 200 代
    --------------------------------------------
    (5, 50)       58        331       340
    (6, 36)       3         110       294
    (5, 37)       3         110       294
    (6, 38)       3         121       260
    (6, 46)       69        97        173
    (5, 49)       60        123       153
    (2, 61)       16        21        21
    (2, 60)       16        21        21
    (1, 62)       16        21        21
    (1, 59)       16        21        21

  20 次里有 8 次改完等于没改,12 次改出了动静。
  有动静的那 12 次,第 200 代的差异在 17 到 340 格之间(满盘 4096 格)。

  改成没改的那几次不是算错了:翻的那一格本来就是孤零零的,
  它边上没有邻居,两代之内自己就死了,等于没翻过。
  可要是翻在正热闹的地方,第 20 代最多差 69 格,
  第 200 代最多差到 340 格 —— 差异从一格扩散成了一片。

  规则里没有一个字提到「传播」,差异却自己散开了。
  这就是局部规则最反直觉的地方:写得下,推不动。

  换个随机种子,同样的密度、同样的规则:

    八个种子 500 代后存活数:355  117  184  215  386  138  232  178
    最多 386,最少 117,差 3.3 倍

  规则完全一样、密度完全一样,只换了随机种子,结果差这么多。
  这就是为什么人工生命的论文里很少写「第 500 代一定是多少格」,
  只写「大概落在哪个区间」「有没有进入循环」。

[4] 顺便量一下:同一套规则,两种写法差多少

  两个写法算的是同一件事,都是 200 代:

    一个格子一个格子地算:2.333 秒
    整张棋盘一起算:      0.0587 秒
    快了 40 倍

  慢的那种每代数 64x64x9 = 36864 次加法,全在 Python 里一次一次做;
  快的那种把这九次加法交给 numpy 的整块运算,一次算完 4096 个格子。
  人工生命模型的特点就在这里:算式简单到可以一行写完,
  但要跑几万代,就得靠这种「整块算」的办法。

[5] 兰顿蚂蚁:规则比生命游戏还短

  一只蚂蚁站在一张空白纸上,面朝上。它只做一件事:

    脚下是白格:右转 90 度,把这一格涂黑,往前走一格
    脚下是黑格:左转 90 度,把这一格涂白,往前走一格

  纸不设边界,可以一直往外走。跑三万步:

    第几步      黑格数    蚂蚁位置
    ----------------------------------------
    1000        118       (-6, 8)
    5000        344       (10, 2)
    10000       720       (-10, -16)
    11000       834       (14, -34)
    15000       1298      (88, -114)
    20000       1872      (184, -210)
    30000       3026      (380, -402)

  规矩只有两条,纸一开始是空的,没有任何图案藏在那里。
  可跑到将近一万步,蚂蚁的轨迹突然开始重复。

[6] 第 9976 步:轨迹开始重复

  拿现在的蚂蚁位置和 104 步之前比,差值:

    第 5000 步前后:飘忽不定,有时往左上,有时往右下
    第 9976 步之后:每 104 步稳定往右上挪 (2, -2)

  最后一次对不上是在第 9975 步,从第 9976 步起一直成立。
  前 9976 步蚂蚁在原地附近打转,离出发点最远只到过 29 格;
  之后就沿着一条斜线一路走远,不再回头。

  每 4000 步的净位移:

    第     0~ 4000 步:( -14,  +16)
    第  4000~ 8000 步:( +20,  -24)
    第  8000~12000 步:( +26,  -48)
    第 12000~16000 步:( +76,  -74)
    第 16000~20000 步:( +76,  -80)
    第 20000~24000 步:( +80,  -74)
    第 24000~28000 步:( +74,  -80)
    第 28000~30000 步:( +42,  -38)

  从第 9976 步到第 30000 步,一共走了 (390, -388),用了 20024 步。
  平均每步 0.0275 格 —— 慢得离谱,可它不再回头了。
  照这个速度一直走,一百万步要走 2.7 万格。

  有人叫这条重复的轨迹「高速公路」。它不在任何一条规则里,
  是两条规则跑了将近一万步之后,自己撞出来的。

  黑格数的涨法也跟着变:

    前 11000 步:每千步多出 75.8 个
    之后       :每千步多出 115.4 个

  三万个格子,两条规则,一步都不许跳。
  人工生命要说的就是这件事:复杂不必来自复杂的规则,
  只要规则是局部的、时间是够长的,结构会自己长出来。

第一段没用循环去数邻居,而是把整张棋盘错位相加。np.roll 把数组整体平移, 越界的那一行会从另一头接回来 —— 这正好就是「边界环绕」。 八次平移叠起来,每个格子的邻居数就出来了。 第二段那张结局表,最该看的是第一行和最后两行。0.02 三个回合死绝,0.30 和 0.40 都进了周期, 0.50 和 0.60 反而还在乱动。密度这个参数不是「越多越热闹」,它把系统推进了不同的相。 第三段最能说明「局部规则推不出全局」。20 次里有 8 次改完毫无影响,因为翻的那格是孤立的; 剩下 12 次里,最大一次把一成棋盘改掉了。同一条规则、同样是一次翻转,后果差了两个数量级。 第四段那个 40 倍是附带的。逐格算每代 36864 次加法,全在 Python 里做; 整块算把这九次加法交给 numpy。 同一条规则两种写法差 40 倍 —— 人工生命模型能跑到几万代,靠的就是这个。 第五段换模型。注意蚂蚁那条规则里没有「记忆」:它每次只读脚下这一格的颜色。 可它走出来的轨迹,第 9976 步之后是严格周期的。 第六段是这一节的高潮。「最后一次对不上在第 9975 步」这句话的意思是: 从第 9976 步到三万步,两万步里误差为零。一条没有任何记忆的规则,跑出了两万步的严格规律。 最后那句「黑格数的涨法也变了」别跳过。从 75.8 涨到 115.4, 说明进入高速公路之后它每步留下的黑格更密。这不是调参调出来的,是轨迹自己变了。

30 秒实验

  1. 把 gen 里的 (n == 3) 改成 (n == 2 or n == 3),重跑第二段。 这条改动让空格更容易变活,0.02 那行应该不再是 0 —— 密度低也能烧起来。
  2. 把蚂蚁那两条规则里的「右转」和「左转」对调,重跑第五段。 轨迹会整体镜像,高速公路的位移从 (2, -2) 变成 (-2, 2),起点步数基本不变。方向翻了个个儿, 长度一样。
  3. 把第六段那句「和 104 步之前比」改成「和 208 步之前比」。 位移应该正好翻倍成 (4, -4) —— 104 是这个周期的最小正周期,任何整数倍都成立。

常见误区

  1. 以为规则简单,行为就一定简单。生命游戏两条规则,跑出了周期 2、周期 3、周期 4 加整体平移, 以及根本看不出周期的混沌。规则的长度跟行为的复杂度没有关系。
  2. 以为跑一遍就能总结出规律。换八个随机种子,500 代后的存活数差 3.3 倍。 这类系统只能给区间和趋势,给不了定值。看到有人写「第 500 代恰好 115 格」, 那是一个种子的结果,不是规则的性质。
  3. 以为生命游戏只是个玩具。它被证明是图灵完备的,能模拟任何计算机。 「玩具」和「通用计算模型」在这里是同一件东西。
  4. 以为「涌现」是个褒义词。它只描述「整体行为不能从单条规则直接读出来」这件事, 不含任何「神奇」的意思。兰顿蚂蚁的高速公路是涌现,城市堵车也是涌现。
自测三问(点开对答案) 1. 生命游戏的规则表为什么只有 140 种「有用的」邻域? —— 因为规则只看两件事:自己死活、邻居几个。 **每格只有死活两态,所以邻域是 2 的 9 次方 = 512 种。**512 种里, 死格邻居正好 3 个有 56 种组合会出生;活格邻居 2 或 3 个有 84 种组合会续命。 **加起来 140 种,占 27.3%,其余 372 种下一代都是死格。** 2. 为什么改一个格子的后果,有时是零,有时是三百多格? —— 因为要看那一格周围有没有「活的东西」可以接上。 **孤立的格子翻过来,按规则两代之内自己就死了,等于没翻。**翻在振荡子或滑翔机旁边, 差异会搭上正在活动的结构,一路带下去。**差别不在「改了多少」,在「改在哪儿」。** 3. 兰顿蚂蚁只记脚下这一格,为什么能修出两万步的严格周期? —— 因为**黑格集合本身就是记忆**。 蚂蚁不记路,但它每走一步就改一格颜色,这些颜色累积在纸上,成了外部记忆。 **第 9976 步之后,它踩到的图案已经进入一个 104 步的循环 —— 每转一圈图案整体挪 (2, -2),** **于是看起来就是一直往右上走。**记忆不在蚂蚁身上,在环境里。

生命游戏里每个格子自己就是一个自动机,只有死活两个状态。 下一节把自动机缩小到一个,摊开它的内部 —— 一张表,几行数据,仅此而已。

相关笔记