提交 7b4f1444 编写于 作者: W wizardforcel

2021-01-07 23:28:06

上级 093f4aa9
......@@ -48,6 +48,21 @@
+ [11 语言学数据管理](docs/nlp-py-2e/11.md)
+ [后记:语言的挑战](docs/nlp-py-2e/12.md)
+ [索引](docs/nlp-py-2e/14.md)
+ [复杂性思维 中文第二版](docs/think-comp-2e-zh/README.md)
+ [一、复杂性科学](docs/think-comp-2e-zh/1.md)
+ [二、图](docs/think-comp-2e-zh/2.md)
+ [三、小世界图](docs/think-comp-2e-zh/3.md)
+ [四、无标度网络](docs/think-comp-2e-zh/4.md)
+ [五、细胞自动机](docs/think-comp-2e-zh/5.md)
+ [六、生命游戏](docs/think-comp-2e-zh/6.md)
+ [七、物理建模](docs/think-comp-2e-zh/7.md)
+ [八、自组织临界](docs/think-comp-2e-zh/8.md)
+ [九、基于智能体的模型](docs/think-comp-2e-zh/9.md)
+ [十、兽群、鸟群和交通堵塞](docs/think-comp-2e-zh/10.md)
+ [十一、进化](docs/think-comp-2e-zh/11.md)
+ [十二、合作进化](docs/think-comp-2e-zh/12.md)
+ [附录 A、算法分析](docs/think-comp-2e-zh/a.md)
+ [附录 B、阅读列表](docs/think-comp-2e-zh/b.md)
+ [TutorialsPoint NumPy 教程](docs/tutorialspoint-numpy.md)
+ [NumPy 秘籍中文第二版](docs/numpy-cookbook-2e/README.md)
+ [零、前言](docs/numpy-cookbook-2e/ch00.md)
......
# 一、复杂性科学
> 原文:[Chapter 1 Complexity Science](http://greenteapress.com/complexity2/html/thinkcomplexity2002.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
这本书的论点是,复杂性科学是一种“新型科学”,我借鉴自 Stephen Wolfram。
2002年,Wolfram 发表了 “新科学”一文,在这里介绍了他和其他人在细胞自动机上的工作,并描述了一种用于计算系统研究的科学方法。在之后的章节中,我们会回顾 Wolfram,但是现在我打算将他的标题用于更广泛的东西。
我认为复杂性是新的,不是因为它将科学工具应用到一个新的主题,而是因为它使用不同的工具,允许不同种类的工作,并最终改变了我们认为是“科学”的东西。
为了证明差异,我将从经典科学的一个例子开始:假设有人问你为什么行星轨道是椭圆形的。你可以引用万有引力的牛顿定律,并用它来写出描述行星运动的微分方程。然后,你可以求解微分方程,并展示出解是椭圆。证明完毕!
大多数人发现这种解释令人满意。它包括一个数学推导 - 所以它有一些严格的证明 - 它解释了具体的观察,椭圆轨道,通过诉诸一般的原则,引力。
让我用另一种解释来对比一下。假设你搬到像底特律这样种族隔离的城市,你想知道为什么这样。如果你做一些研究,你可能会发现 Thomas Schelling 的一篇文章,称为“分离动态模型”,它提出了一个简单的种族隔离模型:
这里是我对这个模型的描述:
+ 城市的谢林模型是一个单元格数组,每个单元格代表一个房子。这些房子被两种“智能体”占据,标有红色和蓝色,数量大致相等。大约10%的房子是空的。
+ 在任何时间点,智能体可能会高兴或不高兴,这取决于附近的其他智能体。在模型的一个版本中,如果智能体至少有两个邻居像自己一样,则智能体很高兴,如果邻居是一个或者零个,则智能体不高兴。
+ 这个模拟通过随机选择一个智能体来运行,并检查它是否快乐。如果是的话,没有任何反应 如果不是,智能体随机选择一个未占用的单元格并移动。
如果你从一个完全未分离的模拟城市开始,并在短时间内运行该模型,类似的智能体会聚集到一起。随着时间的流逝,这些社区会增长和合并,直到存在少量的大型社区,大多数智能体都生活在均匀的社区中。
模型中的分离程度令人惊讶,这是真实城市的分离的解释。也许底特律是分离的,因为人们不喜欢人数太多,并且如果他们的社区的组成使他们不开心,将会搬走。
这个解释与行星运动的解释是一样的吗?许多人会说不是,但为什么?
最明显的是,谢林模型是非常抽象的,也就是说不现实的。我们很容易假设,人比行星更复杂,但是当你想想看,行星就像人一样复杂(特别是拥有人的行星)。
这两个系统都很复杂,而且这两个模型都是基于简化的;例如,在行星运动的模型中,我们包含了地球与太阳之间的力,并忽略行星之间的相互作用。
重要的区别是,对于行星运动,我们可以展示,我们忽略的力小于我们包含的力,来捍卫我们的模型。并且我们可以扩展模型,来包含其他相互作用,并显示这种效果很小。对于谢林模型,它难以合理简化。
更糟糕的是,谢林模型不符合任何物理规律,它只使用简单的计算,而不是数学推导。谢林模型不像经典科学,许多人发现它们不那么引人注目,至少一开始是这样。但是,我将尝试演示,这些模型做了大量的实用工作,包括预测,解释和设计。本书的目标之一是解释如何这样做。
## 1.1 范式转变
当我向人们介绍这本书时,别人经常问我,这种新型科学是不是一种范式转变。我不这么认为,并且这里是解释。
Thomas Kuhn 在 1962 年的“科学革命结构 ”中介绍了“范式转变”一词。它是指科学史上的一个过程,其中一个领域的基本假设改变,或者一个理论被另一个理论取代。他列举了哥白尼革命,燃烧的氧气模型取代了燃素说,以及相对论的出现。
复杂性科学的发展不是取代旧的模型,而是(在我看来)标准模型的逐渐转变,它们是各种种类的可接受的模型。
例如,经典模型倾向于以定律为基础,以方程式的形式表示,并通过数学推导求解。复杂性不足的模型通常是基于规则的,表示为计算,而不是由分析来模拟。
不是每个人都认为这些模型令人满意。例如,在 Sync 中,Steven Strogatz 写道了他的萤火虫自发同步模型。他展示了一个演示该现象的仿真,但是写道:
> 对于其它随机的初始条件和其他数量的振荡器,我重复模拟了几十次。每次都会同步 [...] 现在的挑战是证明它。只有可靠的证明才能演示,同步是不可避免的,这种方式计算机都做不到;最好的证明就是澄清为什么它是不可避免的。
Strogatz 是一位数学家,所以他对证明的热情是可以理解的,但他的证明并不能解决这个现象中最有趣的部分。为了证明“同步是不可避免的”,Strogatz 做了几个简化的假设,特别是每个萤火虫可以看到所有其他的萤火虫。
在我看来,解释整个萤火虫族群为何可以同步,尽管事实上他们不能看到彼此,是更有趣的事情。这种全局行为,如何从局部交互中产生,是第(?)章的主题。这些现象的解释通常使用基于智能体的模型,它探索(以难以或不可能使用数学分析或的方式)允许或阻止同步的条件。
我是一名计算机科学家,所以我对计算模型的热情可能并不奇怪。我不是说 Strogatz 是错误的,而是人们对于提出什么问题,和用什么工具来回答他们,有不同的看法。这些意见基于价值判断,所以没有理由能够达成一致。
然而,科学家们对于哪些模型是好的科学,其他哪些是边缘科学,伪科学,或者是非科学,已经有了很大的共识。
我声称,这是本书的核心论点,即这种共识是基于时间变化的标准,复杂性科学的出现反映了这些标准的逐渐转变。
## 1.2 科学模型的轴线
我将经典模型描述为基于物理定律,以方程式表示,并通过数学分析求解的模型;相反,复杂系统的模型通常基于简单的规则并以计算实现。
我们可以将这一趋势看作是沿着两个轴线的转变:
基于方程式 → 基于 模拟
分析 → 计算
这种新的科学方式在其他几个方面是不同的。我在这里介绍他们,所以你知道即将会发生什么,但是在你看到本书后面的例子之前,有一些可能没有任何意义。
连续 → 离散
经典模型倾向于基于连续数学,如微积分;复杂系统的模型通常基于离散数学,包括图和细胞自动机。
线性 → 非线性
经典模型通常是线性的,或者使用非线性系统的线性近似; 复杂性科学对非线性模型更为友好。一个例子是混沌理论。
> 混沌理论在这本书中没有涉及,但是你可以在 <http://en.wikipedia.org/wiki/Chaos> 上阅读它。
确定性 → 随机
经典模型通常是确定性的,这可能反映了底层哲学的确定性,它在第(?)章中讨论。复杂模型往往具有随机性。
抽象 → 具体
在经典模型中,行星是质点,飞机是无摩擦的,牛是球形的(见 <http://en.wikipedia.org/wiki/Spherical_cow>)。像这样的简化通常对于分析是必要的,但是计算模型可能更加现实。
> 译者注:[真空中的球形鸡](http://www.guokr.com/article/50289/)
一,二 → 很多
在天体力学中,两体问题可以通过分析求解;而三体问题不能。经典模型通常限于少量相互作用的元素,复杂性科学作用于较大的复合体(这是名称的来源)。
单一 → 复合
在经典模型中,元素往往是可互换的;复杂模型更经常包含异质性。
这些是概括性的,所以我们不应该过于认真地对待它们。而我并不意味着弃用经典科学。更复杂的模型不一定更好;实际上通常更糟。
此外,我并不是说这些变化是突然的或完全的。相反,它们向着被认为是可接受的,值得尊重的工作的前沿逐渐迁移。过去被怀疑的工具现在很普遍,一些被广泛接受的模型现在受到审查。
例如,当 Appel 和 Haken 在 1976 年证明了四色定理时,他们使用电脑列举了 1,936 个特殊情况,在某种意义上说,这些特例是其证明的前提。当时很多数学家没有把这个定理当成真正的证明。现在计算机辅助证明是常见的,一般(但并非普遍)是可接受的。
相反,大量的经济分析基于人类行为的模型,称为“经济人”,或者一个有逼格的词:“Homo economicus”。基于这种模型的研究数十年间受到高度重视,特别是如果涉及到数学技巧的话。最近,这种模型受到怀疑,而包含不完整信息和有限理性的模型是热门话题。
## 1.3 一种新的的模型
复杂模型通常适用于不同的目的和解释:
预测 → 解释
谢林的分离模型可能揭示了一个复杂的社会现象,但对预测没有用。另一方面,一个简单的天体力学模型可以预测日食,在未来几年内可以精确到秒。
现实主义 → 工具主义
经典模型依赖于现实主义的解释;例如,大多数人接受电子是存在的真实事物。工具主义一种观点,即使他们假设的实体不存在,模型也可以有用。乔治·皮特写道:“所有模型都是错误的,但有些是有用的。”它可能是工具主义的座右铭。
简化论 → 整体论
简化论是一种观点,通过理解其组件来解释系统的行为。例如,元素的周期表是简化论的胜利,因为它用原子中的简单电子模型来解释元素的化学行为。整体论认为,系统层面出现的一些现象不存在于组件层面,不能在组件层面上解释。
我们在第(?)章会回到解释模型,第(?)章会回到工具主义,第(?)章会回到整体论。
## 1.4 一种新的工程
我一直在科学背景下谈论复杂系统,但复杂性也是工程中的变化和社会系统的组织的一个原因和影响:
中心化(集权) → 去中心化(放权)
中心化系统在概念上简单并易于分析,但去中心化系统可能更加强大。例如,万维网中的客户端向中心化服务器发送请求;如果服务器关闭,则这个服务不可用。在对等网络中,每个节点都是客户端和服务器。要取消服务,你必须删除每个 节点。
隔离 → 互动
在经典工程中,大型系统的复杂性通过隔离组件和最小化相互作用进行管理。这仍然是一个重要的工程原理;然而,廉价计算能力的普及,使得组件之间复杂交互的系统的设计变得越来越可行。
一对多 → 多对多
在许多通信系统中,广播服务正在由一些服务扩展,有时是替换。这些服务允许用户彼此通信,并创建,共享和修改内容。
自上而下 → 自下而上
在社会,政治和经济系统方面,许多通常是集中组织的活动现在都是草根运动。即使是分层结构的典范,军队,指挥和控制的也开始下放。
分析 → 计算
在经典工程中,可行的设计空间受到我们分析能力的限制。例如,设计艾菲尔铁塔成为了可能,因为 Gustave Eiffel 开发了新颖的分析技术,特别是用于处理风压负载。现在,用于计算机辅助设计和分析的工具,可以构建几乎可以想象的任何东西。弗兰克·盖里(Frank Gehry)的毕尔包古根汉美术馆(Guggenheim Museum Bilbao)是我最喜欢的例子。
设计 → 搜索
工程有时被描述为,在可行的设计空间中寻找解决方案。越来越多的搜索过程可以自动化。例如,遗传算法在大型设计空间中探索,并发现人类工程师不会想像(或喜欢)的解决方案。最终的遗传算法,演变,不可避免地生成违反人类工程规则的设计。
## 1.5 一种新的思维
我们现在正在深入一个领域,但是我所假设的,科学建模中的标准转变,有关 20 世纪中逻辑和认识论的发展。
亚里士多德逻辑 → 多值逻辑
在传统逻辑中,任何命题都是真或假。这个系统适用于类似数学的证明,但对于许多现实世界的应用而言是失败的(以一种戏剧化的方式)。替代方案包括多值逻辑,模糊逻辑和其他旨在处理不确定性(indeterminacy),模糊性和不确定性(uncertainty)的系统。Bart Kosko 在《模糊思维》(Fuzzy Thinking)中讨论了一些这种系统。
频率论的概率 → 贝叶斯主义
贝叶斯概率已经存在了几个世纪,但直到最近才被广泛使用,这是由于廉价计算能力变得可用,以及概率性声明中勉强接受了主观性。莎朗·贝尔奇·麦格雷恩(Sharon Bertsch McGrayne)在《不会死亡的理论》(The Theory That Would Not Die)中介绍了这一历史。
客观 → 主观
启蒙运动和现代主义哲学,建立在对客观真理的信仰上。也就是说,独立于持有他们的人的真理。20 世纪的发展,包括量子力学,哥德尔不完备定理和库恩的科学史研究,都引起了人们对“看似不可避免的主观性”的关注,甚至在“自然科学”和数学中。丽贝卡·戈德斯坦(Rebecca Goldstein)介绍了Gödel对不完备性的证明的历史背景。
物理定律 → 理论 → 模型
有些人区分了定律,理论和模型,但我认为这是一回事。使用“定律”的人很有可能认为,它在客观上是真实的,不可改变的;使用“理论”的人承认它可以修改;而“模型”承认它是基于简化和近似的。
一些被称为“物理定律”的概念是真正的定义;实际上,其他的只是模型的断言,它很好预测或解释了系统的行为。我们在第(?)章中会回到物理定律的本质。
确定性 → 不确定性
确定性是一个观点,所有事件都是由之前事件导致,不可避免。不确定性的形式包括随机性,概率因果和基本不确定性。我们在第(?)章再回到这个主题。
这些趋势并不普遍或完整,但核心观点正沿着这些轴线转变。作为证据,考虑对托马斯·库恩(Thomas Kuhn)的《科学革命的结构》(The Structure of Scientific Revolutions)的反应 ,公布后受到谴责,现在被认为几乎毫无争议。
这些趋势是复杂性科学的因和果。例如,高度抽象的模型现在更容易接受,因为人们预期,每个系统都应该有一个独特的,正确的模型。相反,复杂系统的发展挑战了确定性,和物理定律的相关概念。
本章概述了本书中出现的主题,但在看到示例之前,并不是全部都是有意义的。当你读到本书的最后,你可能会发现,再次阅读本章会有帮助。
# 十、兽群、鸟群和交通堵塞
> 原文:[Chapter 10 Herds, Flocks, and Traffic Jams](http://greenteapress.com/complexity2/html/thinkcomplexity2011.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
本章的代码位于`chap10.ipynb`中,它是本书仓库中的 Jupyter 笔记本。使用此代码的更多信息,请参见第?节。
## 10.1 交通堵塞
是什么导致交通堵塞?在某些情况下,有明显的原因,如事故,车速监视或其他干扰交通的事情。 但其他时候,交通堵塞似乎没有明显的原因。
基于智能体的模型有助于解释自发性交通拥堵。 例如,我根据 Resnick,海龟,白蚁和交通堵塞模型实现了一个简单的高速路模拟。
这是代表“高速路”的类:
```py
class Highway:
def __init__(self, n=10, length=1000, eps=0):
self.length = length
self.eps = eps
# create the drivers
locs = np.linspace(0, length, n, endpoint=False)
self.drivers = [Driver(loc) for loc in locs]
# and link them up
for i in range(n):
j = (i+1) % n
self.drivers[i].next = self.drivers[j]
```
`n`是汽车的数量。
`length`是高速路的长度,默认为 1000(以任意单位)。
`eps`是我们将添加到系统中的随机噪声。
`loc`包含驾驶员的位置;最初它们沿着高速公路等距分布。
驾驶员由`Driver`对象表示。 每个驾驶员都包含前方驾驶员的引用。 高速公路是圆形的,所以最后的驾驶员可以引用第一个。
`step`方法简单;它只是移动每个驾驶员:
```py
def step(self):
for driver in self.drivers:
self.move(driver)
```
这里是`move `方法:
```py
def move(self, driver):
d = self.distance(driver)
# let the driver choose acceleration
acc = driver.choose_acceleration(d)
acc = min(acc, self.max_acc)
acc = max(acc, self.min_acc)
speed = driver.speed + acc
# add random noise to speed
speed *= np.random.uniform(1-self.eps, 1+self.eps)
# keep it nonnegative and under the speed limit
speed = max(speed, 0)
speed = min(speed, self.speed_limit)
# if current speed would collide, stop
if speed > d:
speed = 0
# update speed and loc
driver.speed = speed
driver.loc += speed
```
`d`是驾驶员与前方驾驶员之间的距离。 这个距离被传递给`choose_acceleration`,它规定了驾驶员的行为。 这是司机做出的唯一决定; 其他一切都由模拟的“物理”决定。
+ `acc`是加速度,它受`min_acc``max_acc`限制。 在我的实现中,汽车可以在`max_acc = 1`时加速,在`min_acc = -10`时加速。
+ `speed`是旧的速度加上请求的加速度,但是我们做了一些调整。 首先,我们向速度添加了随机噪音,因为这个世界并不完美。 `eps`决定了噪音的幅度,这是适用于速度的百分比; 例如,如果`eps`为 0.02,则速度乘以 98% 到 102% 之间的随机数。
+ 然后速度限制在 0 到`speed_limit`之间,在我的实现中为 40,所以汽车不允许后退或加速。
+ 如果请求的速度会引起与下一辆车的碰撞,则速度设置为 0。
+ 最后,我们更新驾驶员的速度和`loc`属性。
以下是`Driver`类的定义:
```py
class Driver:
def __init__(self, loc, speed=0):
self.loc = loc
self.speed = speed
def choose_acceleration(self, d):
return 1
```
`loc``speed `属性是驾驶员的位置和速度。
`choose_acceleration`的这个实现非常简单:它总是以最大速率加速。
由于汽车起步距离相等,因此我们预计它们都会加速,直到达到限速,或者直到它们的速度超过它们之间的距离。 此时,至少会发生一次“碰撞”,导致一些汽车停下来。
![](img/10-1.png)
图 10.1:三个时间点中,环形公路上的驾驶员的模拟。 点表示驾驶员的位置;十字表示驾驶员必须刹车来避开另一个驾驶员。
图?展示了该过程中的几个步骤,从 30 辆汽车和`eps = 0.02`开始。 左边是 16 个时间步后的状态,汽车排列成一圈。 由于随机噪音,有些汽车比其他汽车要快,并且间距变得不均匀。
在下一个时间步骤(中),两辆车相撞,用`x`标记表示。
在下一个时间步骤(右),两辆汽车会与已停车的汽车碰撞,我们可以看到最初形成的交通堵塞。 一旦堵塞形成,它就会持续下去,其它汽车从后面靠近并碰撞,而前面的汽车加速离开。
在某些情况下,堵塞本身会向后传播,如果你观看本章的笔记本中的动画,你可以看到它。
## 10.2 随机噪声
![](img/10-2.png)
图 10.2:平均速度和汽车数量的函数,带有三个大小的附加随机噪声
随着汽车数量的增加,交通堵塞变得更加严重。 图?显示了汽车能够达到的平均速度,相对于汽车数量的函数。
最上面那行显示`eps = 0`的结果;也就是说,速度没有随机变化。 如果汽车数量少于 25 辆,则汽车之间的间隔大于 40,这样汽车可以达到并保持 40 的最大速度。超过 25 辆汽车形成交通堵塞,平均速度迅速下降。
这种效果是仿真物理学的直接结果,所以它不应该令人惊讶。 如果道路的长度为 1000,则`n`个车辆之间的间距为`1000 / n`。 而且由于汽车的行驶速度不超过前面的空间,所以我们预计,最高平均车速为`1000 / n`或 40,取最小者。
但这是最好的情况。只有少量的随机性,情况会变得更糟。
图?也显示了`eps = 0.001``eps = 0.01`的结果,其对应于 0.1% 和 1% 的速度误差。
即使有少量噪音,高速路的容量也会从 25 降至 20(“容量”是指可以达到并保持速度限制的最大车辆数量。 如果有 1% 的误差,容量会下降到 10。
作为本章结尾的练习之一,你将有机会设计出更好的驾驶员; 也就是说,你将在`choose_acceleration`中尝试不同的策略,并查看你是否可以找到可提高平均速度的驾驶行为。
## 10.3 Boids
1987 年,Craig Reynolds 发表了《兽群,鸟群和鱼群:分布式行为模型》(Flocks, herds and schools: A distributed behavioral model),描述了一个基于智能体的兽群行为模型。 你可以从 <http://www.red3d.com/cwr/papers/1987/boids.html> 下载他的论文。
这种模型中的智能体被称为“boids”,既是“bird-oid”的缩写,又是“bird”的口音发音(虽然 boids 也用于模拟鱼类和集中的陆生动物)。
每个智能体模拟了三种行为:
避免碰撞:
避开障碍物,包括其他鸟类。
鸟群集中:
移向鸟群的中心。
速度匹配:
将速度(速率和方向)与邻近的鸟类对齐。
Boid 只根据局部信息做出决定;每个 boid 只能看到(或注意)其视野范围内的其他 boid。
在本书的仓库中,你会发现`Boids7.py`,它包含我的 boids 实现,部分基于《Flake, The Computational Beauty of Nature》(雪花:自然的计算之美)中的描述。
该程序定义了两个类:`Boid`,实现了 boid 算法,和`World`,包含`Boid`列表和吸引`Boid`的“胡萝卜”列表。
boid 算法使用`get_neighbors`在视野中查找其他 boid:
```py
def get_neighbors(self, others, radius, angle):
boids = []
for other in others:
if other is self:
continue
offset = other.pos - self.pos
# if not in range, skip it
if offset.mag > radius:
continue
# if not within viewing angle, skip it
if self.vel.diff_angle(offset) > angle:
continue
# otherwise add it to the list
boids.append(other)
return boids
```
`get_neighbors`使用向量减法来计算从`self``other`的向量。 这个向量的们是到另一个 boid 的距离。 `diff_angle`计算`self`的速度(也是视线)与另一个 boid 之间的角度。
`center`寻找视野中 boid 的质心,并返回一个指向它的向量:
```py
def center(self, others):
close = self.get_neighbors(others, r_center, a_center)
t = [other.pos for other in close]
if t:
center = sum(t)/len(t)
toward = vector(center - self.pos)
return limit_vector(toward)
else:
return null_vector
```
同样,`avoid`寻找范围内任何障碍物的质心,并返回一个指向它的向量,`copy`将返回当前朝向和邻居的平均朝向之间的差,`love `计算出胡萝卜的朝向。
`set_goal`计算这些目标的加权总和并设定总体目标:
```py
def set_goal(self, boids, carrot):
self.goal = (w_avoid * self.avoid(boids, carrot) +
w_center * self.center(boids) +
w_copy * self.copy(boids) +
w_love * self.love(carrot))
```
最后`move`更新 boid 的速度,位置和姿势。
```py
def move(self, mu=0.1):
self.vel = (1-mu) * self.vel + mu * self.goal
self.vel.mag = 1
self.pos += dt * self.vel
self.axis = b_length * self.vel.norm()
```
新速度是旧速度和目标的加权和。 参数`mu`决定鸟类能够多快地改变速度和方向。 时间步长`dt`决定了 boids 移动的距离。
许多参数影响鸟群行为,包括每个行为的范围,角度和权重以及可操作性`mu`
这些参数决定了 boids 形成和维持鸟群的能力,以及鸟群中运动和组织的模式。 对于某些设置,boids 类似于一群鸟;其他设置类似于鱼群或一片飞虫。
## 10.4 涌现和自由意志
作为一个整体,许多复杂的系统具有它们的组件不具有的属性:
+ 细胞自动机规则 30 是确定性的,控制其演化的规则是完全已知的。 尽管如此,它会生成一个序列,统计上与随机无法区分。
+ 谢林模型中的智能体不是种族主义者,但他们互动的结果就好像他们是。
+ 糖域中的智能体形成对角移动的波浪,尽管智能体不能。
+ 即使汽车正在向前行驶,交通堵塞会向后移动。
+ 兽群和鸟群的行为来自其成员之间的局部互动。
这些例子提出了一个途径,用于解决几个古老而富有挑战性的问题,包括意识和自由意志的问题。
自由意志是做出选择的能力,但是如果我们的身体和大脑受到确定性物理规律的支配,我们的选择就会是确定的。 自由意志的争论无数;我只会提到两个:
+ 威廉·詹姆斯(William James)提出了一个两阶段模型,其中可能的行为由随机过程产生,然后由确定性过程选择。 在这种情况下,我们的行为基本上是不可预测的,因为生成它们的过程包含随机元素。
+ 大卫休谟(David Hume)认为,我们对于做出选择的感知是一种幻觉;在这种情况下,我们的行为是确定性的,因为产生它们的系统是确定性的。
这些论点以相反的方式调解冲突,但他们同意冲突是存在的:如果这些部分是确定性的,那么系统就不会有自由意志。
本书中的复杂系统提出了另一种选择,在选择和决策层面的自由意志,相当于神经元层面的(或更低层次)的决定论。 就像汽车向前行驶时,交通堵塞后退的方式一样,即使神经元没有,人也可以有自由意志。
## 10.5 练习
练习 1
在交通堵塞的模拟中,定义一个类,`BetterDriver`,它继承`Driver`并覆盖`choose_acceleration`。 查看你是否可以定义一个驾驶规则,比`Driver`中的基本实现更好的。 你可能会尝试到达更高的平均速度,或者更少的碰撞。
练习 2
注意:为了做这个练习,你必须安装 VPython,一个用于 3D 显示和动画的库。 如果你使用 Anaconda(我在第?节中推荐过),你可以执行:
```
conda install -c vpython vpython
```
然后运行本书仓库中的`Boids7.py`。 阅读代码来查看,程序开始时定义的参数如何控制 boid 的行为。 试验不同的参数。 如果通过将权重设置为 0 来“关闭”其中一种行为,会发生什么?
为了生成更多类似鸟类的行为,Flake 建议增加第四种行为来保持清晰的视线;换句话说,如果在正前方有另一只鸟,那么它就应该从侧面移开。 你认为这个规则对鸟群的行为有什么影响? 实现它来看看。
练习 3
<http://en.wikipedia.org/wiki/Free_will> 上深入了解自由意志。 自由意志与决定论相容的观点被称为相容论。 相容论最大的挑战之一是“结果论证”(consequence argument)。 什么是结果论证? 根据你在本书中读到的内容,你对结果论证有什么样的反应?
\ No newline at end of file
此差异已折叠。
此差异已折叠。
# 二、图
> 原文:[Chapter 2 Graphs](http://greenteapress.com/complexity2/html/thinkcomplexity2003.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
本书的前三章有关一些模型,它们描述了由组件和组件之间的连接组成的系统。例如,在生态食物网中,组件是物种,连接代表捕食者和猎物的关系。
在本章中,我介绍了 NetworkX,一个用于构建和研究这些模型的 Python 包。我们从 Erdős-Rényi 模型开始,它具有一些有趣的数学属性。在下一章中,我们将介绍更有用的,解释现实系统的模型。
本章的代码在本书仓库中的`chap02.ipynb`中。使用代码的更多信息请参见第(?)章。
## 2.1 图是什么?
![](img/2-1.png)
> 图 2.1:表示社交网络的有向图
对于大多数人来说,图是数据集的视觉表示,如条形图或股票价格对于时间的绘图。这不是本章的内容。
在本章中,图是一个系统的表示,它包含离散的互连元素。元素由节点表示,互连由边表示。
例如,你可以表示一个路线图,每个城市都是一个节点,每个城市之间的路线是一条边。或者你可以表示一个社交网络,每个人是节点,如果他们是朋友,两个人之间有边,否则没有。
在某些图中,边具有长度,成本或权重等属性。例如,在路线图中,边的长度可能代表两个城市之间的距离,或旅行时间。在社交网络中,可能会有不同的边来表示不同种类的关系:朋友,商业伙伴等。
边可以是有向或无向的,这取决于它们表示的关系是不对称的还是对称的。在路线图中,你可能会使用有向边表示单向街道,使用无向边表示双向街道。在某些社交网络,如 Facebook,好友是对称的:如果 A 是 B 的朋友,那么 B 也是 A 的朋友。但在 Twitter 上,“关注”关系并不对称;如果 A 关注了 B,这并不意味着 B 关注 A。因此,你可以使用无向边来表示 Facebook 网络,并将有向边用于 Twitter。
图具有有趣的数学属性,并且有一个称为图论的数学分支,用于研究它们。
图也很有用,因为有许多现实世界的问题可以使用图的算法来解决。例如,Dijkstra 的最短路径算法,是从图中找到某个节点到所有其他节点的最短路径的有效方式。路径是两个节点之间的,带有边的节点序列。
图的节点通常以圆形或方形绘制,边通常以直线绘制。例如,上面的有向图中,节点可能代表在 Twitter 上彼此“关注”的三个人。线的较厚部分表示边的方向。在这个例子中,爱丽丝和鲍勃相互关注,都关注查克,但查克没有关注任何人。
下面的无向图展示了美国东北部的四个城市;边上的标签表示驾驶时间,以小时为单位。在这个例子中,节点的位置大致对应于城市的地理位置,但是通常图的布局是任意的。
## 2.2 NetworkX
![](img/2-2.png)
> 图 2.2:表示城市和高速公路的无向图
为了表示图,我们将使用一个名为 NetworkX 的包,它是 Python 中最常用的网络库。你可以在 <https://networkx.github.io/> 上阅读更多信息,但是我们之后会解释。
我们可以通过导入 NetworkX 和实例化`nx.DiGraph`来创建有向图:
```py
import networkx as nx
G = nx.DiGraph()
```
通常将 NetworkX 导入为`nx`。此时,`G`是一个`DiGraph`对象,不包含节点和边。我们可以使用`add_node`方法添加节点:
```py
G.add_node('Alice')
G.add_node('Bob')
G.add_node('Chuck')
```
现在我们可以使用`nodes`方法获取节点列表:
```py
>>> G.nodes()
['Alice', 'Bob', 'Chuck']
```
添加边的方式几乎相同:
```py
G.add_edge('Alice', 'Bob')
G.add_edge('Alice', 'Chuck')
G.add_edge('Bob', 'Alice')
G.add_edge('Bob', 'Chuck')
```
我们可以使用`edges`来获取边的列表:
```py
>>> G.edges()
[('Alice', 'Bob'), ('Alice', 'Chuck'),
('Bob', 'Alice'), ('Bob', 'Chuck')]
```
NetworkX 提供了几个绘图的功能;`draw_circular`将节点排列成一个圆,并使用边将它们连接:
```py
nx.draw_circular(G,
node_color=COLORS[0],
node_size=2000,
with_labels=True)
```
这就是我用来生成图(?)的代码。`with_labels`选项标注了节点;在下一个例子中,我们将看到如何标注边。
为了产生图(?),我们以一个字典开始,它将每个城市的名称,映射为对应的经纬度:
```py
pos = dict(Albany=(-74, 43),
Boston=(-71, 42),
NYC=(-74, 41),
Philly=(-75, 40))
```
因为这是个无向图,我实例化了`nx.Graph`
```py
G = nx.Graph()
```
之后我可以使用`add_nodes_from`来迭代`pos`的键,并将它们添加为节点。
```py
G.add_nodes_from(pos)
```
下面我会创建一个字典,将每条边映射为对应的驾驶时间。
```py
drive_times = {('Albany', 'Boston'): 3,
('Albany', 'NYC'): 4,
('Boston', 'NYC'): 4,
('NYC', 'Philly'): 2}
```
现在我可以使用`add_edges_from`,它迭代了`drive_times`的键,并将它们添加为边:
```py
G.add_edges_from(drive_times)
```
现在我不使用`draw_circular`,它将节点排列成一个圆圈,而是使用`draw`,它接受`pos`作为第二个参数:
```py
nx.draw(G, pos,
node_color=COLORS[1],
node_shape='s',
node_size=2500,
with_labels=True)
```
`pos`是一个字典,将每个城市映射为其坐标;`draw`使用它来确定节点的位置。
要添加边的标签,我们使用`draw_networkx_edge_labels`
```py
nx.draw_networkx_edge_labels(G, pos,
edge_labels=drive_times)
```
`drive_times`是一个字典,将每条边映射为它们之间的驾驶距离,每条边表示为城市名称的偶对。这就是我生成图(?)的方式。
在这两个例子中,这些节点是字符串,但是通常它们可以是任何可哈希的类型。
## 2.3 随机图
随机图就像它的名字一样:一个随机生成的节点和边的图。当然,有许多随机过程可以生成图,所以有许多种类的随机图。
其中一个更有趣的是 Erdős-Rényi 模型,PaulErdős 和 AlfrédRényi 在 20 世纪 60 年代研究过它。
Erdős-Rényi 图(ER 图)的特征在于两个参数:`n`是节点的数量,`p`是任何两个节点之间存在边的概率。见 <http://en.wikipedia.org/wiki/Erdos-Renyi_model>
Erdős 和 Rényi 研究了这些随机图的属性;其令人惊奇的结果之一就是,随着随机的边被添加,随机图的属性会突然变化。
展示这类转变的一个属性是连通性。如果每个节点到每个其他节点都存在路径,那么无向图是连通的。
在 ER 图中,当`p`较小时,图是连通图的概率非常低,而`p`较大时接近`1`。在这两种状态之间,在`p`的特定值处存在快速转变,表示为`p*`
Erdős 和 Rényi 表明,这个临界值是`p* = lnn / n`,其中`n`是节点数。如果`p < p*`,随机图`G(n, p)`不太可能连通,并且如果`p > p*`,则很可能连通。
为了测试这个说法,我们将开发算法来生成随机图,并检查它们是否连通。
## 2.4 生成图
![](img/2-3.png)
我将首先生成一个完全图,这是一个图,其中每个节点都彼此连接。
这是一个生成器函数,它接收节点列表并枚举所有不同的偶对。如果你不熟悉生成器函数,你可能需要阅读附录?,然后回来。
```py
def all_pairs(nodes):
for i, u in enumerate(nodes):
for j, v in enumerate(nodes):
if i>j:
yield u, v
```
你可以使用`all_pairs`来构造一个完全图。
```py
def make_complete_graph(n):
G = nx.Graph()
nodes = range(n)
G.add_nodes_from(nodes)
G.add_edges_from(all_pairs(nodes))
return G
```
`make_complete_graph`接受节点数`n`,并返回一个新的`Graph`,拥有`n`个节点,所有节点之间都有边。
以下代码生成了一个包含 10 个节点的完全图,并绘制出来。
```py
complete = make_complete_graph(10)
nx.draw_circular(complete,
node_color=COLORS[2],
node_size=1000,
with_labels=True)
```
图(?)显示了结果。不久之后,我们将修改此代码来生成 ER 图,但首先我们将开发函数来检查图是否是连通的。
## 2.5 连通图
如果每个节点到每个其他节点都存在路径,这个图就是连通图。请见<http://en.wikipedia.org/wiki/Connectivity_(graph_theory)>
对于许多涉及图的应用,检查图是否连通是很有用的。幸运的是,有一个简单的算法。
你可以从任何节点起步,并检查是否可以到达所有其他节点。如果你可以到达一个节点`v`,你可以到达`v`的任何一个邻居,他们是`v`通过边连接的任何节点。
`Graph`类提供了一个称为`neighbors`的方法,返回给定节点的邻居列表。例如,在上一节中我们生成的完全图中:
```py
>>> complete.neighbors(0)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
```
假设我们从节点`s`起步。我们可以将`s`标记为“已访问”,然后我们可以标记它的邻居。然后我们标记邻居的邻居,依此类推,直到你无法再到达任何节点。如果访问了所有节点,则图是连通图。
以下是 Python 中的样子:
```py
def reachable_nodes(G, start):
seen = set()
stack = [start]
while stack:
node = stack.pop()
if node not in seen:
seen.add(node)
stack.extend(G.neighbors(node))
return seen
```
`reachable_nodes`接受`Graph`和起始节点`start`,并返回可以从`start`到达的节点集合,他们。
最初,已访问的集合是空的,我们创建一个名为`stack`的列表,跟踪我们发现但尚未处理的节点。最开始,栈包含单个节点`start`
现在,每次在循环中,我们:
+ 从栈中删除一个节点。
+ 如果节点已在`seen`中,我们返回到步骤 1。
+ 否则,我们将节点添加到`seen`,并将其邻居添加到栈。
当栈为空时,我们无法再到达任何节点,所以我们终止了循环并返回。
例如,我们可以找到从节点`0`可到达的,完全图中的所有节点:
```py
>>> reachable_nodes(complete, 0)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
```
最初,栈包含节点`0``seen`是空的。第一次循环中,节点`0`添加到了`seen`,所有其他节点添加到了栈中(因为它们都是节点`0`的邻居)。
下一次循环中,`pop`返回栈中的最后一个元素,即节点`9.`因此,节点`9`被添加到`seen`,并且其邻居被添加到栈。
请注意,同一个节点在栈中可能会出现多次;实际上,具有`k`个邻居的节点将添加到栈`k`次。稍后我们将寻找方法,来使此算法更有效率。
我们可以使用`reachable_nodes`来编写`is_connected`
```py
def is_connected(G):
start = next(G.nodes_iter())
reachable = reachable_nodes(G, start)
return len(reachable) == len(G)
```
`is_connected`通过调用`nodes_iter`来选择一个起始节点,`node_iter`返回一个迭代器对象,并将结果传递给`next`,返回第一个节点。
`reachable`获取了一组节点,它们可以从`start`到达。如果这个集合的大小与图的大小相同,那意味着我们可以访问所有节点,也就是这个图是连通的。
一个完全图是连通的,毫不奇怪:
```py
>>> is_connected(complete)
True
```
下一节中,我们会生成 ER 图,并检查它们是否是连通的。
## 2.6 生成 ER图
![](img/2-4.png)
> 图 2.4:ER 图,`n=10`,`p=0.3`
ER 图`G(n, p) `包含`n`个节点,每对节点以概率为`p`的边连接。生成 ER 图类似于生成完全图。
以下生成器函数枚举所有可能的边,并使用辅助函数`flip`,来选择哪些应添加到图中:
```py
def random_pairs(nodes, p):
for i, u in enumerate(nodes):
for j, v in enumerate(nodes):
if i>j and flip(p):
yield u, v
```
`flip`以给定概率`p`返回`True`,以互补的概率`1-p`返回`False`
```py
from numpy.random import random
def flip(p):
return random() < p
```
最后,`make_random_graph`生成并返回 ER 图`G(n, p)`
```py
def make_random_graph(n, p):
G = nx.Graph()
nodes = range(n)
G.add_nodes_from(nodes)
G.add_edges_from(random_pairs(nodes, p))
return G
```
`make_random_graph`几乎和`make_complete_graph`,唯一的不同是它使用`random_pairs`而不是`all_pairs`
这里是`p=0.3`的例子:
```py
random_graph = make_random_graph(10, 0.3)
```
图(?)展示了结果。这个图是连通图;事实上,大多数`p=10`并且`p=3`的 ER 图都是连通图。在下一节中,我们将看看有多少。
## 2.7 连通性的概率
![](img/2-5.png)
> 图 2.5:连通性的概率,`n=10`,`p`是一个范围。竖直的线展示了预测的临界值。
![](img/2-6.png)
>图 2.6:连通性的概率,`n`是多个值,`p`是一个范围。
对于`n``p`的给定值,我们想知道`G(n, p)`连通的概率。我们可以通过生成大量随机图,来计算有多少个来估计它。就是这样:
```py
def prob_connected(n, p, iters=100):
count = 0
for i in range(iters):
random_graph = make_random_graph(n, p)
if is_connected(random_graph):
count += 1
return count/iters
```
`iters`是我们生成的随机图的数量。随着我们增加`iter`,估计的概率就会更加准确。
```py
>>> prob_connected(10, 0.3, iters=10000)
0.6454
```
在具有这些参数的 10000 个 ER 图中,6498 个是连通的,因此我们估计其中65%是连通的。所以 0.3 接近临界值,在这里连通概率从接近 0 变为接近 1。根据 Erdős 和 Rényi,`p* = lnn / n = 0.23`
我们可以通过估计一系列`p`值的连通概率,来更清楚地了解转变:
```py
import numpy as np
n = 10
ps = np.logspace(-2.5, 0, 11)
ys = [prob_connected(n, p) for p in ps]
```
这是我们看到的使用 NumPy 的第一个例子。按照惯例,我将 NumPy 导入为`np`。函数`logspace`返回从`10 ** -2.5``10 ** 0 = 1`的 11 个元素的数组,在对数刻度上等间隔。
为了计算`y`,我使用列表推导来迭代`ps`的元素,并计算出每个值为`p`的随机图的连通概率。
图(?)展示了结果,竖直的线为`p*`。从 0 到 1 的转变发生在预测的临界值附近。在对数刻度上,这个转变大致对称。
对于较大的`n`值,图(?)展示了类似的结果。随着`n`的增加,临界值越来越小,转变越来越突然。
这些实验与 Erdős 和 Rényi 在其论文中证明的结果一致。
## 2.8 图的算法分析
这一章中,我提出了一个检查图是否连通的算法;在接下来的几章中,我们将再次看到更多的图的算法。并且我们要分析这些算法的性能,了解它们的运行时间如何随着图大小的增加而增长。
如果你还不熟悉算法分析,在你继续之前,你应该阅读附录一。
图算法的增长级别通常表示为顶点数量`n`,以及边数量`m`的函数。
作为一个例子,我们分析从前面的`reachable_nodes`
```py
def reachable_nodes(G, start):
seen = set()
stack = [start]
while stack:
node = stack.pop()
if node not in seen:
seen.add(node)
stack.extend(G.neighbors(node))
return seen
```
每次循环,我们从栈中弹出一个节点;默认情况下,`pop`删除并返回列表的最后一个元素,这是一个常数时间的操作。
接下来我们检查节点是否被已访问,这是一个集合,所以检查成员是常数时间。
如果节点还没有访问,我们添加它是常量时间,然后将邻居添加到栈中,这相对于邻居数量是线性的。
为了使用`n``m`表达运行时间,我们可以将每个节点添加到`seen``stack`的总次数加起来。
每个节点只添加一次,所以添加的总数为`n`
但是节点可能多次被添加到栈,具体取决于它们有多少邻居。如果节点具有`k`个邻居,则它会被添加到栈`k`次。当然,如果它有`k`个邻居,那意味着它拥有`k`个边。
所以添加到栈的总数是边的数量`m`的两倍,由于我们考虑每个边两次。
因此,这个函数的增长级别为`O(n + m)`,我们可以说,即运行时间与`n``m`成正比,以较大者为准。
如果我们知道`n``m`之间的关系,我们可以简化这个表达式。例如,在完全图中,边数是`n(n-1)/ 2`,它属于`O(n^2)`。所以对于一个完全图,`reachable_nodes`是二次于`n`的。
## 2.9 练习
本章的代码在`chap02.ipynb`中,它是本书的仓库中的 Jupyter 笔记本。使用此代码的更多信息,请参阅第(?)节。
练习 1:启动`chap02.ipynb`并运行代码。笔记本中嵌入了一些简单的练习,你可能想尝试一下。
练习 2:我们分析了`reachable_nodes`的性能,并将其分类为`O(n + m)`,其中`n`是节点数,`m`是边数。继续分析,`is_connected`的增长级别是什么?
```py
def is_connected(G):
start = next(G.nodes_iter())
reachable = reachable_nodes(G, start)
return len(reachable) == len(G)
```
练习 3 :在我实现`reachable_nodes`时,你可能很困惑,因为向栈中添加所有邻居而不检查它们是否已访问,明显是低效的。编写一个该函数的版本,在将邻居添加到栈之前检查它们。这个“优化”是否改变了增长级别?它是否使函数更快?
> 译者注:在弹出节点时将其添加到`seen`,在遍历邻居时检查它们是否已访问。
练习 4:
实际上有两种 ER 图。我们在本章中生成的一种,`G(n,p)`的特征是两个参数,节点数量和节点之间的边的概率。
一种替代定义表示为`G(n,m)`,也以两个参数为特征:节点数`n`和边数`m`。在这个定义中,边数是固定的,但它们的位置是随机的。
使用这个替代定义,重复这一章的实验。这里是几个如何处理它的建议:
1. 编写一个名为`m_pairs`的函数,该函数接受节点列表和边数`m`,并返回随机选择的`m`个边。一个简单的方法是,生成所有可能的边的列表,并使用`random.sample`
2. 编写一个名为`make_m_graph`的函数,接受`n``m`,并返回`n`个节点和`m`个边的随机图。
3. 创建一个`prob_connected`的版本,使用`make_m_graph`而不是`make_random_graph`
4. 计算一系列`m`值的连通概率。
与第一类 ER 图的结果相比,该实验的结果如何?
此差异已折叠。
此差异已折叠。
此差异已折叠。
# 六、生命游戏
> 原文:[Chapter 6 Game of Life](http://greenteapress.com/complexity2/html/thinkcomplexity2007.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
在本章中,我们考虑二维细胞自动机,特别是 John Conway 的生命游戏(GoL)。 像上一章中的一些 CA 一样,GoL 遵循简单的规则并产生令人惊讶的复杂行为。 就像沃尔夫勒姆的规则 110 一样,事实证明 GoL 是通用的;也就是说,至少在理论上它可以计算任何可计算的函数。
GoL 的复杂行为引发了科学哲学问题,特别是科学现实主义和工具主义的相关问题。 我讨论这些问题并提出扩展阅读的建议。
在本章的最后,我演示了如何在 Python 中高效实现 GoL。
本章的代码位于本书仓库的`chap06.ipynb`中。 使用代码的更多信息,请参见第?节。
## 6.1 Conway 的生命游戏
首先要研究的细胞自动机之一,也许是有史以来最受欢迎的一种,是称为“生命游戏”的二维 CA,简称 GoL。 它由 John H. Conway 开发并于 1970 年在《科学美国人》(Scientific American)的马丁加德纳(Martin Gardner)专栏中推广。 请参阅 <http://en.wikipedia.org/wiki/Conway_Game_of_Life>
GoL 中的细胞排列在一个二维网格中,两个方向上都有限,或者首尾相接。 双向首尾相接的网格称为环面,因为它在地形上等同于多纳圈的表面。 见 <http://en.wikipedia.org/wiki/Torus>
每个细胞有两个状态 - 生存和死亡 - 和八个邻居 - 东西南北和四个对角线。 这些邻居有时被称为“摩尔邻域”。
就像前面章节中的一维 CA 一样,生命游戏按照规则演变,这就像物理学的简单定律。
在 GoL 中,每个单元格的下一个状态取决于其当前状态和活动邻居的数量。 如果一个细胞是活的,如果它有两个或三个活动邻居就会生存,否则就会死亡。 如果一个细胞是死的,它将保持死亡,除非它恰好有三个邻居。
下表总结了这些规则:
| 当前状态 | 邻居数量 | 下一个状态 |
| --- | --- | --- |
| 生存 | 2–3 | 生存 |
| 生存 | 0–1, 4–8 | 死亡 |
| 死亡 | 3 | 生存 |
| 死亡 | 0–2, 4–8 | 死亡 |
这种行为与真正的细胞生长大致类似:分离或过度拥挤的细胞死亡;它们在中等密度下蓬勃成长。
GoL 很受欢迎,因为:
有简单的初始条件产生令人惊讶的复杂行为。
有许多有趣的稳定图案:有些摆动(以不同的周期),有些像 Wolfram 的 CA 规则 110 中的飞船一样移动。
和规则 110 一样,GoL 是图灵完整的。
另一个产生兴趣的因素是康威的猜测 - 没有可以使活细胞数量无限增长的初始条件 - 以及他向任何可以证明或否定它的人提供的 50 美元赏金。
最后,计算机日益增加的可用性,使得自动化计算并以图形方式显示结果成为可能。
## 6.2 生命图案
![](img/6-1.png)
图 6.1:一个静态图案,叫做“蜂巢”(beehive)
![](img/6-2.png)
图 6.2:一个振荡图案,叫做“蟾蜍”(toad)
![](img/6-3.png)
图 6.3:一个飞船,叫做“滑翔机”(glider)
如果从随机起始状态运行 GoL,可能会出现一些稳定图案。随着时间的推移,人们已经确定了这些图案并给了它们名字
例如,图?展示了一种称为“蜂巢”的稳定图案。蜂巢中的每个细胞都有两个或三个邻居,所以它们都能存活下来,蜂巢旁边的死细胞都没有三个邻居,所以没有新细胞诞生。
其他图案在“振荡”;也就是说,它们随着时间而改变,但最终返回到它们的起始状态(只要它们不与另一个图案冲突)。例如,图?展示了一种称为“蟾蜍”的图案,它是在两种状态之间交替的振荡图案。这个振荡图案的“周期”是二。
最后,一些图案振荡并返回到起始状态,但在空间中移动。因为这些图案似乎在移动,所以它们被称为“飞船”。
图?展示了一艘名为“滑翔机”的飞船。经过四段时间后,滑翔机回到起始位置,并向下和向右移动一个单位。
根据起始方向,滑翔机可以沿着四条对角线中的任何一条移动。还有其它的水平和垂直移动的飞船。
人们花费了大量时间来查找和命名这些图案。如果你搜索网页,你会发现很多收藏品。
## 6.3 Conwey 的推测
从最初的条件来看,GoL 迅速达到稳定状态,活细胞数量几乎不变(可能带有一些振荡)。
![](img/6-4.png)
图 6.4:r-pentomino 的开始和最终状态
但是一些简单的开始条件,需要很长时间才能稳定下来,并产生令人惊讶的活细胞数量。 这些模式被称为“Methuselahs”,因为它们很长寿。
其中最简单的是 r-pentomino,它只有五个细胞,形状大致为字母“r”。 图?显示了 r-pentomino 的初始状态和 1103 步后的最终状态。
这种状态是“最终的”,因为所有剩余图案是稳定的,振荡的或滑翔机,它们永远不会与另一种图案相冲突。 r-pentomino 总共产生 6 个滑翔机,8 个积木(block),4 个闪光灯(blinker),4 个蜂巢,1 个小艇(boat),1 个轮船(ship)和 1 个面包(loaf)。
![](img/6-5.png)
图 6.5:Gosper 的滑翔机枪,产生滑翔机流。
长寿图案的存在,使得康威怀疑是否存在从未稳定的初始图案。 他猜想没有,但他描述了两种证明他是错误的图案,“枪”(gun)和“蒸汽火车”(puffer train)。 枪是稳定的模式,定期产生飞船 - 随着飞船流从源位置移动,活细胞的数量无限增长。 蒸汽火车是一种将活细胞留在尾部的平移图案。
事实证明,这两种模式都存在。 由 Bill Gosper 领导的一个小组发现了第一个,它是现在称为 Gosper's Gun 的滑翔枪,如图所示。 Gosper 还发现了第一个蒸汽火车。
这两种类型都有很多图案,但它们很难设计或找到。 这不是巧合。 Conway 选择了 GoL 的规则,这样他的猜想就不会明显为真或假。 在二维 CA 的所有可能规则中,大多数产生简单的行为:大多数初始条件快速稳定或无限增长。 通过避免无趣的 CA,Conway 也避免了 Wolfram 的一类和二类行为,并且可能还有三类。
如果我们相信 Wolfram 的计算等价原则,我们预计 GoL 会属于第四类,而且是这样。 生命游戏在 1982 年被证明了图灵的完整性(1983年也是独立的)。 从那时起,几个人构建了 GoL 模式,实现了图灵机或另一台已知图灵完备的机器。
## 6.4 现实主义
GoL中的稳定模式很难不被注意,特别是那些移动的模式。 将它们视为持久的实体是很自然的事,但请记住,CA 是由细胞构成的;没有蟾蜍或面包这样的东西。 滑翔机和其他飞船甚至更不真实,因为随着时间的推移,它们甚至不由相同的细胞组成。 所以这些图案就像星座一样。 我们这样看待他们,因为我们善于观察图案,或者因为我们有活跃的想象力,但他们不是真实的。
对嘛?
好吧,不是那样。 我们认为“真实”的许多实体,也是规模较小的实体的持久图案。 飓风只是气流的模式,但我们给了他们个人名称。 而人就像滑翔机,随着时间的推移不是由相同细胞组成的。 但即使你更换了你体内的每一个细胞,我们也认为你是同一个人。
这不是一个新观察 - 大约在 2500 年前,赫拉克利特(Heraclitus)指出你不能在同一条河流中两次 - 但是出现在生命游戏中的实体,是思考哲学现实主义的实用测试用例。
在哲学的背景下,现实主义是这样一种观点,即世界中的实体存在与人类的感知和概念无关。 “感知”是指我们从感官中获得的信息,而“概念”是指我们形成的世界的心智模式。 例如,我们的视觉系统将一些东西感知为场景的二维投影,我们的大脑使用该图像构建场景中物体的三维模型。
科学实在论与科学理论和他们所假设的实体有关。 如果一个理论使用实体的属性和行为来表达,那么这个理论假设了一个实体。 例如,电磁学的理论用电场和磁场表示。 经济学的一些理论以供给,需求和市场力量来表达。 生物学的理论是用基因来表达的。
但这些实体是真实的吗? 也就是说,它们存在于独立于我们和我们的理论的世界吗?
再次,我发现,在一系列强度中陈述哲学立场是有用的;这里有四个科学现实主义的陈述,强度逐渐增加:
SR1:
对于它们接近现实的程度,科学理论为真或假,但没有理论是完全正确的。 一些所假设的实体可能是真实的,但没有原则性的方式来说出哪些是真实的。
SR2:
随着科学的进步,我们的理论会变得更加逼近现实。 至少有一些所假定的实体是已知真实的。
SR3:
有些理论是完全正确的;其他近似真实。 真实理论所假设的实体,以及近似真实理论中的一些实体是真实的。
SR4:
如果一个理论正确地描述了现实,那么这个理论就是真的,否则就是假。真实理论所假设的实体是真实的;其他不是。
SR4 非常强,可能是站不住脚的;通过这样一个严格的标准,几乎所有当前的理论都被认为是错误的。 大多数现实主义者会接受 SR1 和 SR3 之间的东西。
## 6.5 工具主义
但 SR1 很弱以至于它接近工具主义,这是一种观点,我们不能说理论是真是假,因为我们不知道理论是否符合现实。 理论是我们用于我们的目的的工具;在适用于其目的的程度上,理论是有用的,或者不是。
要看看你是否对工具主义感到满意,请考虑以下陈述:
“生命游戏中的实体并不是真实的;他们只是人们赋予可爱的名字的细胞图案。”
“飓风只是一种气流模式,但它是一种有用的描述,因为它可以让我们进行有关天气的预测和沟通。”
“像本我和超我这样的弗洛伊德实体并不是真实的,但它们是思考和交流心理学的有用工具(或者至少有些人是这么认为的)。”
“电磁场是我们最好的电磁理论中的假设实体,但它们并不真实。 我们可以构建其他理论,而不用场的假设,这也是一样有用的。”
“我们认为,世界上的许多物体都是像星座一样的任意集合。 例如,蘑菇只是真菌的子实体,其中大部分是在地下生长的,几乎不连续的细胞网络。 我们由于实际原因专注于蘑菇,如可见性和可爱。”
“有些物体边界清晰,但很多都是模糊的。 例如,哪些分子是你身体的一部分:你的肺里的空气? 你的胃里的食物? 你血液中的营养物质? 细胞中的营养物质? 细胞中的水? 细胞的结构部分? 头发? 死皮? 污垢? 你的皮肤上的细菌? 你的肠道细菌?线粒体? 当你称量自己时,你包含了多少这些分子? 根据离散对象构想世界是有用的,但我们确定的实体并不是真实的。”
对于每一个你同意的陈述,给自己一分。 如果你的分数超过 4 分,你可能会成为一名工具主义者!
如果你比其他人更喜欢这些陈述,那么问问你自己为什么。 这些情景中的哪些差异会影响你的反应? 你能否在他们之间做出原则性区分?
工具主义的更多信息,请参阅 <http://en.wikipedia.org/wiki/Instrumentalism>
## 6.6 实现
本章最后的练习要求你尝试和修改生命游戏,并实现其他二维细胞自动机。 本节介绍 GoL 的实现,你可以将其用作实验的起始位置。
为了表示细胞的状态,我使用类型为`uint8`的 NumPy 数组,它是一个 8 位无符号整数。 例如,下面这行创建一个 10 乘 10 的数组,并用 0 和 1 的随机值进行初始化。
```py
a = np.random.randint(2, size=(10, 10)).astype(np.uint8)
```
我们可以用几种方法计算 GoL 规则。 最简单的方法是使用`for`循环遍历数组的行和列:
```py
b = np.zeros_like(a)
rows, cols = a.shape
for i in range(1, rows-1):
for j in range(1, cols-1):
state = a[i, j]
neighbors = a[i-1:i+2, j-1:j+2]
k = np.sum(neighbors) - state
if state:
if k==2 or k==3:
b[i, j] = 1
else:
if k == 3:
b[i, j] = 1
```
最初,`b`是一个与`a`大小相同的零数组。 每次循环中,状态是中心细胞的条件,邻居是`3×3`的邻域。 `k`是活动邻居的数量(不包括中心细胞)。 嵌套的`if`语句评估 GoL 规则并相应地激活`b`中的细胞。
这个实现是规则的直接翻译,但它是冗长而缓慢的。 我们可以使用互相关做得更好,正如我们在第?节中看到的那样。 在那里,我们使用`np.correlate`来计算一维相关。 现在,为了计算二维相关,我们将使用`scipy.signal`中的`correlate2d`,它是一个 SciPy 模块,提供信号处理的相关函数:
```py
from scipy.signal import correlate2d
kernel = np.array([[1, 1, 1],
[1, 0, 1],
[1, 1, 1]])
c = correlate2d(a, kernel, mode='same')
```
在一维相关的背景下,我们称之为“窗口”的内容,在二维相关的背景下被称为“核”,但其想法是相同的:`correlate2d`将核和数组相乘来选择一个邻域,然后将结果加起来。 这会核选择中心细胞周围的 8 个邻居。
`correlate2d`将核应用于数组中的每个位置。 使用`mode ='same'`时,结果与`a`的大小相同。
现在我们可以使用逻辑运算符来计算规则:
```py
b = (c==3) | (c==2) & a
b = b.astype(np.uint8)
```
第一行计算了一个布尔数组,其中应该有活细胞的地方为`True`,其他地方为`False`。 然后,`astype`将布尔数组转换为整数数组。
这个版本更快,也许够好,但是我们可以通过修改核来简化它:
```py
kernel = np.array([[1, 1, 1],
[1,10, 1],
[1, 1, 1]])
c = correlate2d(a, kernel, mode='same')
b = (c==3) | (c==12) | (c==13)
b = b.astype(np.uint8)
```
这个版本核的包含中心单元并赋予其权重 10。如果中心单元为 0,则结果介于 0 和 8 之间; 如果中心单元为 1,则结果在 10 到 18 之间。使用这个核,我们可以简化逻辑运算,只选择值为 3,12 和 13 的细胞。
这看起来可能不是什么大的改进,但它允许进一步简化:使用这个核,我们可以使用一个表来查找细胞的值,就像我们在第?节中所做的那样。
```py
table = np.zeros(20, dtype=np.uint8)
table[[3, 12, 13]] = 1
c = correlate2d(a, kernel, mode='same')
b = table[c]
```
除了位置 3,12 和 13 以外,表格中的任何位置都为零。当我们使用`c`作为表格中的索引时,NumPy 执行逐元素查找;也就是说,它从`c`中获取每个值,在表中查找它并将结果放入`b`中。
这个版本比其他版本更快更简洁, 唯一的缺点是需要更多的解释。
包含在本书仓库中的`Life.py`提供了一个封装规则实现的`Life`类。 如果你执行`Life.py`,你应该看到一个“蒸汽火车”的动画,这是一种飞船,在其尾部留下一串碎屑。
## 6.7 练习
练习 1
本章的代码位于本书仓库的 Jupyter 笔记本`chap06.ipynb`中。 打开这个笔记本,阅读代码,然后运行单元格。 你可以使用这个笔记本来练习本章的练习。 我的解决方案在`chap06soln.ipynb`中。
练习 2
以随机状态启动 GoL 并运行它直至稳定。 你可以识别哪些稳定的图案?
练习 3
许多命名图案都以便携式文件格式提供。 修改`Life.py`来解析其中一种格式并初始化网格。
练习 4
一种最长寿的小型图案是“兔子”,它以 9 个活动细胞开始,需要 17 331 个步骤来稳定。 你可以在 <http://www.conwaylife.com/wiki/Rabbits> 获取各种格式的初始状态。 加载此状态并运行它。
练习 5
在我的实现中,`Life`类基于一个名为`Cell2D`的父类,`LifeViewer`基于`Cell2DViewer`。 你可以使用这些基类来实现其他二维细胞自动机。
例如,GoL 的一个变体叫做“Highlife”,与 GoL 规则相同,另外还有一条规则:有 6 个邻居的死亡细胞会变活。
编写一个名为`Highlife`的类,该类继承自`Cell2D`并实现这个版本的规则。 另外编写一个名为`HighlifeViewer`的类,该类继承自`Cell2DViewer`并尝试以不同的方式来展示结果。 作为一个简单的例子,使用不同的颜色表。
`Highlife`中更有趣的图案之一是复制器(replicator)。 使用`add_cells`和复制器初始化`Highlife`并查看它做了什么。
练习 6
如果将图灵机扩展到两个维度,或者将读写头添加到二维 CA,则结果是称为 Turmite 的细胞自动机。由于读写头移动的方式,它以白蚁(termite)命名,但拼写错误是对 Alan Turing 的敬意。
最着名的 Turmite 是 1986 年由 Chris Langton 发现的兰顿的蚂蚁(Langton's Ant)。请见 <http://en.wikipedia.org/wiki/Langton_ant>
蚂蚁(ant)是一个具有四种状态的读写头,你可以将其视为面向东、西、南或北。细胞有两种状态,黑色和白色。
规则很简单。在每个时间步骤中,蚂蚁检查它所在单元格的颜色。如果是黑色,蚂蚁转向右转,将细胞变成白色,并向前移动一个格子。如果细胞是白色的,蚂蚁会向左转,将细胞变成黑色,然后向前移动。
给定一个简单的世界,一组简单的规则,并且只有一个可移动的部分,你可能会期望看到简单的行为 - 但你现在应该更清楚。从所有的白色细胞开始,在进入周期为 104 步的循环之前,兰顿的蚂蚁以看似随机的方式移动超过 10000 步。每个循环后,蚂蚁都会沿对角线平移,因此会留下一条称为“高速路”的踪迹。
编写兰顿的蚂蚁的实现。
# 七、物理建模
> 原文:[Chapter 7 Physical modeling](http://greenteapress.com/complexity2/html/thinkcomplexity2008.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
到目前为止,我们所看到的细胞自动机不是物理模型;也就是说,他们不打算描述现实世界中的系统。 但是一些 CA 用作物理模型。
在本章中,我们考虑一个 CA,它模拟扩散(散开)并相互反应的化学物质,这是 Alan Turing 提出的过程,用于解释一些动物模式如何发展。
我们将试验一种 CA,它模拟通过多孔材料的渗透液体,例如通过咖啡渣的水。 这个模型是展示相变行为和分形几何的几个模型中的第一个,我将解释这两者的含义。
本章的代码位于本书仓库的`chap07.ipynb`中。 使用代码的更多信息,请参见第?节。
## 7.1 扩散
1952 年,艾伦图灵发表了一篇名为“形态发生的化学基础”的论文,该论文描述了涉及两种化学物质的系统行为,它们在空间中扩散并相互反应。 他表明,这些系统根据扩散和反应速率产生了广泛的模式,并推测像这样的系统可能是生物生长过程中的重要机制,特别是动物着色模式的发展。
图灵模型基于微分方程,但也可以使用细胞自动机来实现。
但在我们开始使用图灵模型之前,我们先从简单的事情开始:只有一种化学物质的扩散系统。 我们将使用 2-D CA,其中每个细胞的状态是连续的数量(通常在 0 和 1 之间),表示化学物质的浓度。
我们将通过比较每个细胞与其邻居的均值,来建模扩散过程。 如果中心细胞的浓度超过领域均值,则化学物质从中心流向邻居。 如果中心细胞的浓度较低,则化学物质以另一种方式流动。
以下核计算每个细胞与其邻居均值之间的差异:
```py
kernel = np.array([[0, 1, 0],
[1,-4, 1],
[0, 1, 0]])
```
使用`np.correlate2d`,我们可以将这个核应用于数组中的每个细胞:
```py
c = correlate2d(array, kernel, mode='same')
```
我们将使用一个扩散常数`r`,它关联了浓度差与流速:
```py
array += r * c
```
![](img/7-1.png)
图 7.1:0,5 和 10 步后的简单扩散模型
图?显示 CA 的结果,其中`n=9, r=0.1`,除了中间的“岛”外,初始浓度为 0。 该图显示了 CA 的启动状态,以及 5 步和 10 步之后的状态。 化学物质从中心向外扩散,直到各处浓度相同。
## 7.2 反应扩散
现在我们添加第二种化学物。 我将定义一个新对象`ReactionDiffusion`,它包含两个数组,每个化学物对应一个:
```py
class ReactionDiffusion(Cell2D):
def __init__(self, n, m, params):
self.params = params
self.array = np.ones((n, m), dtype=float)
self.array2 = np.zeros((n, m), dtype=float)
island(self.array2, val=0.1, noise=0.1)
```
`n``m`是数组中的行数和列数。 `params`是参数元组,下面我会解释它。
数组代表第一种化学物质`A`的浓度,它最初是无处不在的。
`array2`表示`B`的浓度,除了中间的一个岛屿,它初始为零,并且由`island`初始化:
```py
def island(a, val, noise):
n, m = a.shape
r = min(n, m) // 20
a[n//2-r:n//2+r, m//2-r:m//2+r] = val
a += noise * np.random.random((n, m))
```
岛的半径`r``n``m`的二十分之一,以较小者为准。 岛的高度是`val`,在这个例子中是`0.1`。 此外,随机均匀噪声(值为 0 到`noise`)添加到整个数组。
这里是更新数组的`step `函数:
```py
def step(self):
"""Executes one time step."""
A = self.array
B = self.array2
ra, rb, f, k = self.params
cA = correlate2d(A, self.kernel, **self.options)
cB = correlate2d(B, self.kernel, **self.options)
reaction = A * B**2
self.array += ra * cA - reaction + f * (1-A)
self.array2 += rb * cB + reaction - (f+k) * B
```
参数是
`ra`
`A`的扩散速率(类似于前一节中的`r`)。
`rb`
`B`的扩散速率。在该模型的大多数版本中,`rb`约为`ra`的一半。
`f`
进给速率,控制着`A`添加到系统的速度。
`k`
移除速率,控制`B`从系统中移除的速度。
现在让我们仔细看看更新语句:
```py
reaction = A * B**2
self.array += ra * cA - reaction + f * (1-A)
self.array2 += rb * cB + reaction - (f+k) * B
```
数组`cA``cB`是将扩散核应用于`A``B`的结果。乘以`ra``rb`得出进入或离开每个细胞的扩散速率。
表达式`A * B ** 2`表示`A``B`相互反应的比率。 假设反应消耗`A`并产生`B`,我们在第一个方程中减去这个项并在第二个方程中加上它。
表达式`f * (1-A)`决定`A`加入系统的速率。 当`A`接近 0 时,最大进给速率为`f`。 当`A`接近 1 时,进给速率下降到零。
最后,表达式`(f+k) * B`决定`B`从系统中移除的速率。 当`B`接近 0 时,该比率变为零。
只要速率参数不太高,`A``B`的值通常保持在 0 和 1 之间。
![](img/7-2.png)
图 7.2:1000,2000 和 4000 步之后的反应扩散模型,参数为`f=0.035``k=0.057`
使用不同的参数,该模型可以产生类似于各种动物身上的条纹和斑点的图案。 在某些情况下,相似性是惊人的,特别是当进给和移除参数在空间上变化时。
对于本节中的所有模拟,`ra = 0.5``rb = 0.25`
图?显示了`f=0.035``k=0.057`的结果,`B`的浓度以较暗的颜色显示。 有了这些参数,系统就向稳定状态演化,在`B`的黑色背景上有`A`的光点。
![](img/7-3.png)
图 7.3:1000,2000 和 4000 步之后的反应扩散模型,参数为`f=0.055``k=0.062`
图?显示了`f = 0.055``k = 0.062`的结果,在`A`的背景上产生了珊瑚样的`B`
![](img/7-4.png)
图 7.4:1000,2000 和 4000 步之后的反应扩散模型,参数为`f=0.039``k=0.065`
图?显示了`f = 0.039``k = 0.065`的结果。 在类似于有丝分裂的过程中,这些参数产生的`B`点生长和分裂,最后形成稳定的等距点图案。
1952 年以来,观察和实验为图灵猜想提供了一些支持。 目前为止,看起来许多动物图案实际上由某种反应扩散过程形成,但尚未证实。
## 7.3 渗流
渗流是流体流过半多孔材料的过程。 实例包括岩层中的油,纸中的水和微孔中的氢气。 渗流模型也用于研究不是严格渗滤的系统,包括流行病和电阻网络。 请见 <http://en.wikipedia.org/wiki/Percolation_theory>
渗流模型常常用随机图来表示,就像我们在第?章中看到的那样,但它们也可以用细胞自动机表示。 在接下来的几节中,我们将探索模拟渗流的 2-D CA。
在这个模型中:
+ 最初,每个细胞是概率为`p`的“多孔”或者“无孔”,并且除了顶部那行是“湿的”之外,所有单元都是“干的”。
+ 在每个时间步骤中,如果多孔细胞至少有一个湿的邻居,它会变湿。 非多孔细胞保持干燥。
+ 模拟运行直至达到不再有细胞改变状态的“固定点”。
如果存在从顶部到底部的湿细胞路径,我们说 CA 具有“渗流簇”。
渗流的一个主要问题是,找到渗流簇的概率以及它如何依赖于`p`。 这个问题可能会让你想起第?节,其中我们计算了随机 ER 图连接的概率。 我们会看到这两个模型之间的几个关系。
我定义了一个新类来表示渗流模型:
```py
class Percolation(Cell2D):
def __init__(self, n, m, p):
self.p = p
self.array = np.random.choice([0, 1], (n, m), p=[1-p, p])
self.array[0] = 5
```
`n``m`是 CA 中的行数和列数。 `p`是细胞为多孔的概率。
CA 的状态存储在数组中,该数组使用`np.random.choice`初始化,以概率`p`选择 1(多孔),以概率`1-p`选择 0(无孔)。 顶部那行的状态设置为 5,表示一个湿细胞。
在每个时间步骤中,我们使用 4 细胞邻域(不包括对角线)来检查任何多孔细胞是否拥有湿的邻居。 这是核:
```py
kernel = np.array([[0, 1, 0],
[1, 0, 1],
[0, 1, 0]])
```
这里是`step `函数:
`correlate2d`将邻居的状态相加,如果至少有一个邻居是湿的,那么至少大于 5。 最后一行寻找多孔的细胞,`a == 1`,并且至少有一个湿邻居,`c >= 5`,并将它们的状态设置为 5,这代表湿的。
![](img/7-5.png)
图 7.5:渗流模型的前三个步骤,其中`n=10``p=0.5`
图?显示了`n = 10``p = 0.5`的渗流模型的前几个步骤。 非多孔细胞为白色,多孔细胞为浅色,湿细胞为深色。
## 7.4 相变
现在让我们测试 CA 是否包含渗流簇。
```py
def test_perc(perc):
num_wet = perc.num_wet()
num_steps = 0
while True:
perc.step()
num_steps += 1
if perc.bottom_row_wet():
return True, num_steps
new_num_wet = perc.num_wet()
if new_num_wet == num_wet:
return False, num_steps
num_wet = new_num_wet
```
`test_perc`接受`Percolation`对象作为参数。 每次循环中,它都会使 CA 前进一个时间步骤。 它检查底部那行,看看有没有湿的细胞;如果有,它返回`True`,表示存在渗透簇,以及`num_steps`,它是到达底部所需的时间步数。
在每个时间步骤中,它还计算湿细胞的数量并检查自上一步以来数量是否增加。 如果没有,我们已经到达了固定点,而没有找到一个渗流簇,所以我们返回`False`
为了估计渗流簇的概率,我们生成许多随机初始状态并测试它们:
```py
def estimate_prob_percolating(p=0.5, n=100, iters=100):
count = 0
for i in range(iters):
perc = Percolation(n, p=p)
flag, _ = test_perc(perc)
if flag:
count += 1
return count / iters
```
`estimate_prob_percolating`使用给定的`p``n`值生成 100 个 CA,并调用`test_perc`来查看其中有多少个具有渗流簇。 返回值是拥有的 CA 的比例。
`p = 0.55`时,渗滤簇的概率接近于 0。`p = 0.60`时,它约为 70%,而在`p = 0.65`时,它接近于 1。这种快速转变表明`p`的临界值接近 0.6。
我们可以更精确地使用随机游走来估计临界值。 从`p`的初始值开始,我们构造一个`Percolation`对象并检查它是否具有渗透簇。 如果是这样,`p`可能太高,所以我们减少它。 如果不是,`p`可能太低,所以我们增加它。
这里是代码:
```py
def find_critical(p=0.6, n=100, iters=100):
ps = [p]
for i in range(iters):
perc = Percolation(n=n, p=p)
flag, _ = test_perc(perc)
if flag:
p -= 0.005
else:
p += 0.005
ps.append(p)
return ps
```
`find_critical``p`的给定值开始并上下调整,返回值的列表。 当`n = 100`时,`ps`的平均值约为 0.59,对于从 50 到 400 的`n`值,这个临界值似乎是一样的。
临界值附近的行为的快速变化称为相变,类似于物理系统中的相变,例如水在冰点处从液体变为固体的方式。
在处于或接近临界点时,各种各样的系统展示了一组共同的行为和特征。这些行为被统称为临界现象。 在下一节中,我们将探究其中的一个:分形几何。
## 7.5 分形
为了理解分形,我们必须从维度开始。
对于简单的几何对象,维度根据缩放行为而定义。 例如,如果正方形的边长为`l`,则其面积为`l ** 2`。 指数 2 表示正方形是二维的。 同样,如果立方体的边长为`l`,则其体积为`l ** 3`,这表示立方体是三维的。
更一般来说,我们可以通过测量一个对象的“尺寸”(通过一些定义),将对象的维度估计为线性度量的函数。
例如,我将通过测量一维细胞自动机的面积(“开”细胞的总数),将它的维度估计为行数的函数。
![](img/7-6.png)
图 7.6:32 个时间步之后,规则为 20,50 和 18 的一维 CA。
图?展示了三个一维 CA,就像我们在第?节中看到的那样。 规则 20(左)产生一组看似线性的细胞,所以我们预计它是一维的。 规则 50(中)产生类似于三角形的东西,所以我们预计它是二维的。 规则 18(右)也产生类似三角形的东西,但密度不均匀,所以其缩放行为并不明显。
我将用以下函数来估计这些 CA 的维度,该函数计算每个时间步之后的细胞数。 它返回一个元组列表,其中每个元组包含`i``i ** 2`,用于比较,以及细胞总数。
```py
def count_cells(rule, n=500):
ca = Cell1D(rule, n)
ca.start_single()
res = []
for i in range(1, n):
cells = np.sum(ca.array)
res.append((i, i**2, cells))
ca.step()
return res
```
![](img/7-7.png)
图 7.7:规则 20,50 和 18 的“开”细胞的数量与时间步数。
图?展示以双对数刻度绘制的结果。
在每幅图中,顶部虚线表示`y = i ** 2`。 两边取对数,我们得到`logy = 2logi`。 由于该数字在双对数刻度上,因此直线的斜率为2。
同样,底部的虚线表示`y = i`。 在双对数刻度上,直线的斜率为 1。
规则 20(左)每两个时间步骤产生三个细胞,所以`i`步后的细胞总数为`y = 1.5 i`。 两边取对数,我们得到`logy = log1.5 + logi`,所以在双对数刻度上,我们期待一条斜率为 1 的线。实际上,线的估计的斜率为 1.01。
规则 50(中)在第`i`个时间步骤中产生`i + 1`个新细胞,因此`i`步之后的细胞总数为`y = i ** 2 + i`。 如果我们忽略第二项并取两边的对数,我们有`logy ~ 2 logi`,所以当`i`变大时,我们预计看到一条斜率为 2 的线。事实上,估计的斜率为 1.97。
最后,对于规则 18(右),估计的斜率大约是 1.57,这显然不是 1,2 或任何其他整数。 这表明规则 18 生成的图案具有“分数维度”;也就是说,它是一个分形。
## 7.6 分形和渗流模型
![](img/7-8.png)
图 7.8:`p=0.6``n=100, 200, 300`的渗流模型
现在让我们回到渗透模型。 图?展示了`p = 0.6``n = 100, 200, 300`的渗流模型中的湿细胞簇。非正式来说,它们类似于在自然界和数学模型中看到的分形模式。
为了估计它们的分形维度,我们可以运行一系列尺寸的 CA,计算每个渗流簇中湿细胞的数量,然后看看随着我们增加 CA 的大小,细胞计数的规模如何增长。
以下循环运行了模拟:
```py
for size in sizes:
perc = Percolation(size, p=p)
flag, _ = test_perc(perc)
if flag:
num_filled = perc.num_wet() - size
res.append((size, size**2, num_filled))
```
结果是元组列表,其中每个元组包含`size ``size ** 2`,用于比较,以及渗流簇中的细胞数(不包括顶行中的初始湿细胞)。
![](img/7-9.png)
图 7.9:渗流簇中的细胞数量与 CA 大小
图?展示了 10 到 100 范围内的结果。点展示了每个渗流簇中的细胞数。 拟合这些点的线的斜率大约为 1.85,这表明当`p`接近临界值时,渗滤簇实际上是分形的。
`p`大于临界值时,几乎每个多孔细胞都被填充,因此湿单元的数量仅为`p * size ** 2`,它的维度为 2。
`p`远小于临界值时,湿细胞的数量与 CA 的线性大小成比例,因此它的维度为 1。
## 7.7 练习
练习 1
在第?节中,我们发现 CA 规则 18 产生了一个分形。 你能找到其他产生分形的一维 CA 吗?
注意:`Cell1D.py`中的`Cell1D`对象不会从左边绕到右边,对于某些规则它在边界上创建了手工艺品 [?]。你可能想要使用`Wrap1D`,它是`Cell1D`的子类。 它也在`Cell1D.py`中定义。
练习 2
1990 年,Bak,Chen 和 Tang 提出了一种细胞自动机,它是一种森林火灾的抽象模型。 每个细胞处于三种状态之一:空,被树占用或着火。
CA 的规则是:
+ 空细胞以概率`p`被占用。
+ 如果任何一个邻居着火,那么带有树的细胞就会燃烧。
+ 即使没有邻居着火,带有树的细胞自发燃烧,概率为`f`
+ 在下一个时间步骤中,着火的细胞变为空细胞。
编写一个实现这个模型的程序。 你可能想要继承`Cell2D`。 参数的常用值为`p = 0.01``f = 0.001`,但你可能想要尝试其他值。
从随机初始条件开始,运行 CA 直到它达到稳定状态,树的数量不再持续增加或减少。
在稳定状态下,森林分形的几何形状是什么? 它的分形维度是多少?
此差异已折叠。
此差异已折叠。
此差异已折叠。
+ [复杂性思维 中文第二版](README.md)
+ [一、复杂性科学](1.md)
+ [二、图](2.md)
+ [三、小世界图](3.md)
+ [四、无标度网络](4.md)
+ [五、细胞自动机](5.md)
+ [六、生命游戏](6.md)
+ [七、物理建模](7.md)
+ [八、自组织临界](8.md)
+ [九、基于智能体的模型](9.md)
+ [十、兽群、鸟群和交通堵塞](10.md)
+ [十一、进化](11.md)
+ [十二、合作进化](12.md)
+ [附录 A、算法分析](a.md)
+ [附录 B、阅读列表](b.md)
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册