☰
用AnyLogic实现谢林模型:从微观规则到宏观隔离的智能体仿真
2026/10/4 6:46:38 网站建设 项目流程

几年前第一次在课堂上见到谢林模型的时候,我就觉得这东西特别有意思——明明每个人只想在自己的小圈子里待得舒服一点,结果整个城市却自发地"画"出了泾渭分明的一块块区域。这种微观动机和宏观结果之间的巨大落差,比很多复杂的热力学公式都来得震撼。最近用 AnyLogic 重新把这个经典模型完整搭了一遍,发现把它落地成仿真程序的过程中,真正难的不是抄公式,而是怎么把"邻居不满意就搬家"这种模糊的社会规则,翻译成Agent能执行的行为逻辑。

这篇博文就围绕"AnyLogic 建立谢林模型"来写,我会从建模思路、主体设计、空间设置、参数标定到结果解读一步步拆开讲,顺便把手边踩过的坑和排查思路一起整理出来。无论你是刚接触智能体仿真、正在做课程作业,还是想用ABM验证自己的社会观察,这篇内容都能直接照着做。AnyLogic本身提供了免费的教育版许可,用来练这个模型绰绰有余,预算方面的顾虑可以先放一边。

1. 项目背景与核心思路拆解

1.1 谢林模型要回答什么问题

谢林模型是经济学家托马斯·谢林在上世纪六七十年代提出的一个思想实验,核心场景是这样的:在一个棋盘式的小世界里住着一批居民,居民分成两类(比如红色和蓝色),每个人都会观察自己周围邻居的构成,如果发现同类的比例低于自己的心理底线,他就会搬走,去别的地方碰碰运气。

关键点在于,这里的"同类偏好"并不需要很强。谢林本人的经典设置是:只要周边邻居中至少有30%和自己同类,人们就愿意留下来。听起来是个相当宽容的社会,如果整个城市初始是均匀混合的,按理说大家应该和平共处,对吧?可仿真跑下去,结局却是同类住在一起,区域分化非常明显。模型的魅力就在这里——它用一个极简的规则,演出了种族隔离、社区分群、文化圈层这类复杂现象的生成过程。

我这次用 AnyLogic 重做一遍,目标不是把模型改得多花哨,而是完整走一遍"定义Agent—放置到空间—建立感知规则—循环演化—收集宏观指标"的仿真链路,并且把每一步的参数选择和界面设置都说清楚。对初学者来说,这个模型是理解智能体建模(ABM)最好的入门案例,没有太多额外业务干扰,却能牵出模型可复现性、随机种子、参数敏感性这些做仿真躲不开的话题。

1.2 为什么选 AnyLogic 来搭谢林模型

市面上可以搭ABM的工具不算少,NetLogo名气大,Mesa在Python圈子里很流行,但我个人更推荐用 AnyLogic 来做谢林模型,理由有三个。

第一,AnyLogic 支持连续空间和道路网络两种Agent移动模式。谢林模型的经典设定是"搬离旧住所、搬去新位置",传统棋盘模型往往借助网格移动,而 AnyLogic 里可以把Agent放在连续的三百平米空间内,通过坐标直接改变位置。这一下就绕开了很多模拟类库中"网格坐标"与"真实坐标"转换的麻烦,也方便后续做车辆或行人级的扩展。

第二,AnyLogic 的展示层和业务逻辑层是天然分离的。Agent可以在后台执行状态逻辑,同时在展示层画一个圆、一个图标甚至一张图片。调试时你能很直观地看到"谁在搬家、搬到哪、周围邻居结构怎么变",这种实时反馈对理解涌现机制帮助很大。

第三,AnyLogic 自带参数实验(Parameter Variation)实验框架,改参数跑大批次仿真不用自己写循环。这一点在分析"不同容忍度阈值对隔离程度的影响"时特别省事,我后面会用到的结果曲线就是这么批量跑出来的。

AnyLogic 的授权版本虽然区分了试用版、研究版和行业版,但建模和跑小规模仿真的核心功能在教育许可下都能用,谢林模型这个规模完全不受限制。

2. 环境准备与模型骨架搭建

2.1 创建模型与环境配置

先在 AnyLogic 里新建一个项目。打开软件后选择 File -> New -> Model,给模型命名,比如我这次用的是 "SchellingModel"。

保存位置选一个自己容易找到的目录即可。接下来要特别注意选择模型类型:在新建弹窗中,Model time 选 "Virtual time" 就行,因为我们关注的不是具体物理时间,而是迭代步数;Agent 建模方式可以保持默认。AnyLogic 支持三种仿真建模方法(离散事件、系统动力学、智能体),创建模型时可以把 "Agent-based model" 勾上,这样 Main 会被默认视为智能体空间,后面添加群体会更顺手。

模型单位方面,把时间单位设为 "simulation time units" 就能满足需求。这样做的好处是,后面设置搬运动作时,不需要纠结秒和分钟之间的换算,直接把一次状态更新视为一个时间单位,逻辑清爽得多。

2.2 创建居民智能体类型与群体

在项目树里右键 "Model" 节点,选择 "New -> Agent Type",给这个类型起名叫 "Resident"。

这个 Resident 就代表一个居民。它需要两个核心字段:一个是类别标签,我习惯用 int 型变量 "group" 来存,1代表红色,2代表蓝色;另一个是容忍度阈值,用 double 型变量 "threshold" 存,含义是"周围至少有多少比例的同组邻居才愿意留下"。

回到 Main 画布,从 Agent 面板拖入一个 "Population" 对象。在属性设置里,把 Population 的 Agent 类型指向 Resident,初始人数我设为 100,其中 group=1 和 group=2 各占一半。你可以直接填写 "equiprobable (1, 2)" 这样的小表达式,也可以写个循环在仿真启动时给每个Agent随机赋值。

这里我比较建议在 Main 的 "On startup" 操作中写初始化代码,而不是在 Agent 类型里写死。原因很简单:以后做参数实验时,你希望同一套模型能通过改变参数生成不同的初始分布,而不是每次改代码。比如:

for (Resident r : population) { r.group = uniform(0, 1) < 0.5 ? 1 : 2; r.threshold = 0.3; // 这里后续可以改成参数 }

这样把所有初始化逻辑集中在一个地方,统计起来也方便。值得说明的是,AnyLogic 的 Population 对象是支持直接按 Agent 类型生成实例的,运行时它会自动把所有实例添加到 Main 空间,不需要自己 new 对象。

2.3 布局与展示对象的设置

要让模型看起来直观,需要给 Agent 设置自定义展示。在任何 Agent 类型的 "Presentation" 画布上,我放了一个椭圆,大小设为 8x8 像素,填色条件为:

  • 如果 group==1,填充红色;如果 group==2,填充蓝色。
  • 如果该居民已经"满意"(同组邻居比例达到阈值),可以画一个绿色边框;如果不满意,边框画成灰色。

这样在动画里你能一眼看出哪些居民处于即将搬家的状态。这算是仿真建模里很讨巧的做法——把Agent状态映射到展示属性上,运行的时候相当于可视化监控,省了额外写日志的功夫。

Main 画布上还需要设置连续空间。在 Main 的 "Space" 属性页里,勾选 "Continuous space",把宽度和高度都设为 30 到 40 之间的值。空间的左上角是 (0, 0),水平向右为 X 正方向,垂直向下为 Y 正方向。后续让居民搬到新位置时,就是给 Agent 的 X、Y 属性赋新值。

3. 核心机制实现:主体、空间与动态规则

3.1 居民Agent的行为逻辑设计

谢林模型的核心逻辑其实很短:每个居民定期环顾四周,如果觉得不满意,就搬一次家。

在 AnyLogic 里实现这个逻辑,最简单的方式是给 Resident 添加一个 "事件" 或者 "循环事件"。我在 Resident 的 "Event" 目录下加了一个 "checkEvent",触发方式设为循环,触发时间可以设置为模型启动后 1 个时间单位触发一次,之后每次间隔 1 个时间单位。

事件触发时调用一个函数,比如叫 "checkSatisfactionAndMove()"。函数的伪代码如下:

// 函数 checkSatisfactionAndMove() List<Resident> neighbors = getNeighborsWithinRadius(2.0); int sameCount = 0; int totalCount = neighbors.size(); for (Resident other : neighbors) { if (other.group == this.group) sameCount++; } double sameRatio = totalCount == 0 ? 1.0 : (double) sameCount / totalCount; if (sameRatio < this.threshold) { moveToRandomLocation(); }

注意,这里把"没有任何邻居"这种情况算作满意,是避免模型在一开始某个人被孤立到角落时反复搬家产生死循环。这个细节看起来小,实际仿真中影响很大,建议都做防呆处理。

3.2 邻居怎么定义:半径选择与边界问题

邻居的定义方式直接影响整个模型的行为。经典谢林模型一般用摩尔邻域(周围8个格子)或者距离不超过某个值的邻居。我在连续空间里用的是"半径内邻居"——即以当前居民为中心,半径为 2 个空间单位画一个圆,落入这个圆内的其他居民都算邻居。

如果空间只有 30x30,人口 100,平均每个人周围应该会有几个邻居,密度大致可以手动估:总面积 900,人口 100,平均每 9 个平方单位一个人,半径 2 的圆面积约 12.56,所以平均邻居数大约 1.4 个。这个数字并不高,很容易出现"周围没人"的状况。所以在实际调参时,我把半径设为 2.5 或 3,但是也要注意半径过大后整个模型很快就会收敛——因为大家都在很快地找到合适的邻居。

AnyLogic 中getNeighborsWithinRadius这个方法是空间管理器提供的遍历方法,本质上是在空间中做邻近查询。如果你后续换成道路网络空间,同样可以调用getNeighborsWithinDistanceOnNetwork获取网络距离内的Agent,逻辑相似但更贴近城市尺度。

边界问题也要留意。默认情况下,Main 空间的边界是"硬边界",Agent 不能超出左上、右下坐标范围。因为所有移动都发生在空间内部,所以目前不存在越界风险。但如果后面给模型加"迁出城市"机制,就得额外处理边界之外的新坐标,否则Agent会直接被抛到空间外部的缓冲区,动画上看就像消失了一样。

3.3 搬家机制:随机移动与避让策略

移动操作在连续空间里就是改坐标值。最简单的实现是:在当前坐标基础上加减一个随机偏移量,保证新位置仍然落在空间范围内:

double newX = uniform(0, spaceWidth); double newY = uniform(0, spaceHeight); setX(newX); setY(newY);

这种完全随机搬家会导致两个问题:一是Agent可能在原地附近打转,搬家效率低;二是没人能保证新位置不会和另一个Agent重叠。在谢林模型里,重叠并不是致命的,因为判断邻居时不会把自己算进去,视觉上只是两个圆叠在一起,不仔细看根本注意不到。

为了让搬家过程更像真实居民择房,我做了一个简单版"理性搬房":随机采样若干候选位置,选一个能让同组邻居比例最高的位置搬过去。这个策略实现只要多写几行循环,但效果立竿见影——收敛速度提升,隔离模式也更清晰。

Resident[] candidates = new Resident[10]; // 随机生成10个候选位置,并保存到临时数组 // 对每个候选位置,计算"假如搬过去后的同组邻居比例",选择最大值的那个

核心思路就是"择优迁移"。现实里人们搬家的确会先看看目标小区环境,而不是闭眼扔骰子。用这个策略后,模型就不太需要跑很多迭代才稳定。

3.4 状态反馈与统计逻辑

为了让结果有说服力,光靠动画直观感受不够,需要在 Main 上放几个统计量。我加了:

  • double 变量averageSatisfaction:所有居民满意度的均值。
  • int 变量unhappyCount:当前不满意的居民数。
  • History 图显示averageSatisfaction随时间的曲线。

在每次循环事件里(也就是每个时间单位结束后),Main 上放一个 "Event" 触发统计更新:

double total = 0; int unhappy = 0; for (Resident r : population) { double ratio = r.getSameGroupRatio(); total += ratio; if (ratio < r.threshold) unhappy++; } averageSatisfaction = total / population.size(); unhappyCount = unhappy;

其中getSameGroupRatio是 Resident 里的公开方法,封装了邻域统计逻辑。注意这个方法每次调用都要遍历邻居,在 100 个 Agent 规模下完全没问题,但如果以后扩展到几千个Agent,就要考虑空间索引,或者降低统计频率——别在每个Agent的每个事件里都全量遍历空间。

4. 仿真运行与结果解读

4.1 基础参数与运行策略

把上述逻辑连起来后,可以设定一组基础参数先跑一遍:

  • 总居民数:100
  • 类别比例:50% 红 / 50% 蓝
  • 邻居判定半径:2.5
  • 容忍度阈值:0.3
  • 初始位置:完全随机
  • 循环周期:1 个时间单位

运行后,差不多在 10 到 30 个时间单位内,动画上就会出现明显的"红蓝分块"区域。

我建议第一次跑不要加任何图表,先把动画速度调慢,观察Agent是怎么一步步搬家的。你会注意到,初始随机分布下大家均匀地混在一起,并没有特别强的抱团趋势。但只要有那么几个Agent因为周围同类太少而搬家,腾出的位置让原本就聚集的区域变得更有吸引力,新搬来的人又提高了那个区域的同组比例,形成正反馈。整个系统的隔离程度不会线性上升,而会在某个节点突然变得很明显,类似相变。

4.2 容忍度阈值怎么调

谢林模型最经典的研究对象就是容忍度阈值。我采用 AnyLogic 的参数实验功能,对阈值从 0.1 到 0.6 每隔 0.05 取值,每次跑 1000 个时间单位,最后统计每个Agent的同组邻居比例平均值。

实际跑下来:

  • 阈值在 0.1 到 0.2 之间时,系统基本维持混合状态,只有零星局部抱团。
  • 阈值到 0.3 到 0.4 之间时,全图出现比较清晰的大块分区。
  • 阈值超过 0.5 后,系统几乎完全"分裂",两类群体各自占据空间的不同区域,中间只留下很窄的接触带。

这个结果和学术文献里的经典结论高度一致——即使整体偏好很温和(比如只要三成同类),宏观隔离依然会自发出现。原因是搬迁行为是序贯的、不可逆的,先搬走的人改变了周围人的统计环境,导致更多人搬走,链式反应放大了初始条件中的微小涨落。

用 AnyLogic 跑参数实验时,注意输出指标要设置为 "averageSatisfaction" 或隔离指数。这里给大家推荐一个更直观的指标:同类邻居占比的平均值,它在完全混合时接近 0.5,完全隔离时接近 1.0。你可以在 Main 里加一个函数计算这个值,然后在参数实验里选为输出。

4.3 随机种子和可复现性

很多第一次接触仿真的人都会忽略随机种子,结果同一组参数跑两次出来画面完全不一样,就以为模型写错了。其实这不是 bug,是随机性导致的正常现象。

在 AnyLogic 里,可以通过设置随机种子来保证可复现性。在实验属性页的 "Random seed" 里固定一个值,比如 2024,那么每次运行这个实验都会产生同样的随机数序列,动画和统计曲线也完全一致。

我习惯在测试阶段固定随机种子,方便和队友对齐问题;在正式做参数扫描时,则对每个参数组合跑 5 到 10 个不同的种子,把结果取平均值,这样得到的关系曲线更稳。最终文章或报告里呈现的图,都是多随机种子平均后的结果,而不是单次运气好看到的样张。

4.4 从仿真结果中读出了什么

仿真的最终输出不只是"会出现隔离"这个结论。你仔细看动画过程会发现,隔离模式的形成有两种路径:

一种是从均匀分布中自行演化的。系统一开始没有任何预先设定的社区,完全靠微观搬房规则,慢慢长出一个个聚居区。这个现象能很好地解释为什么现实生活中很多社区并没有经过刻意规划,却依然自动呈现同质化。

另一种是初始条件中就存在核心聚集点时,隔离会沿着该核心向外扩展。你可以试试把人口位置设置成几个小簇,运行结果会变成簇之间彼此排斥、边界越来越清晰。这对理解城市更新、旧城改造中"原生社群"的延续性和排斥性很有启发。

我在写小结的时候,把这两条路径对比着写,模型本身的价值就凸显出来了:它不是什么复杂社会预言,而是一个揭示"低强度偏好也能产生高强度分离"的机制演示。你要如何去理解这种机制,取决于使用它的场景。

5. 常见问题与排查技巧实录

5.1 Agent 不搬家怎么办

最常见的表现是:模型跑很久,动画上几乎没有任何移动,统计曲线一条直线。

先检查一下 checkEvent 是否真的被触发了。比如事件没设置成循环模式,或循环周期设置太长,都会造成不搬家的假象。再检查阈值是否设得太低——如果 threshold=0,那么每个Agent只要周围存在任意一个同类就满意,大多数情况下不会搬。

还有一个容易忽略的点:如果你的moveToRandomLocation函数里用了 setX、setY 直接赋值,但 Agent 类型勾选了 "Movable" 属性,这个属性本身不影响,但若你用的是道路网络空间,就不应该用 setX、setY 了,而是要通过 "Car" 或 "Pedestrian" 库里的移动逻辑来控制。这里我调整为忠实于连续空间模型,所以 move 直接改坐标是可行的。

5.2 邻居统计总是为零

这类问题出现在getNeighborsWithinRadius返回空列表。一种情况是Agent所在的群组没有添加到 Main 的连续空间里——在 AnyLogic 里,只要 Population 对象放在 Main 画布上,并且 Main 勾选了连续空间,Agent就会被自动放进空间。如果群体不在 Main 画布上,或者被放到了另一个 Agent 类型的内部空间,那查询就找不到邻居。

另一种情况是半径设置太小。如果地图是 30x30,半径设为 0.5,邻居可能确实很少,我的做法是先把半径调到 3 试试,如果统计值正常再逐步缩小。

调试时可以在错误街区打印日志,比如在统计函数里临时加一句:

System.out.println("Agent " + this.getId() + ", neighbors=" + neighbors.size());

运行后在控制台中看输出。日志里能发现某些 Agent 邻居数一直为 1 或 2,这通常是空间边缘效应,可以考虑把空间边界设置为环绕(torus)模式来消除。

5.3 模型跑得好慢或死循环

如果每轮每人都要遍历一次邻居,复杂度是 O(n^2)。100人没问题,但如果你试图扩展到 5000 人,每轮 2500 万次邻居判断,动画就会卡成幻灯片。建议:

  • 统计频率下降为每 5 个或 10 个时间单位一次;
  • 搬家事件也适当降低频率,不必每个时间单位都让所有 Agent 重新评估;
  • Java 内存给够,但更主要是减少全空间遍历。

死循环的问题出在我前面提过的"空邻居判断"上。如果不把"无邻居"视为满足,Agent 周围无人时会一直搬家,搬哪都还是没人,一直触发同一逻辑,形成死循环。在阈值比较低、人口密度低的场景尤其明显,我建议代码里加一个最大搬家次数限制,比如同一个Agent在 5 个时间单位内最多搬 10 次,防止极端情况下程序停不下来。

5.4 图表不更新或曲线形状怪异

AnyLogic 中的 History 图默认绘图方式是随时间追加数据。如果你的 Event 里只是修改变量值,图不会自动刷新——必须调用图表的addDataFromFunction或使用"变量变化"触发方式。我通常这样做:

在 History 图属性中,设置 "X axis" 为 time,"Y axis" 选择一个函数,比如getAverageSatisfaction()。这样每次时间推进时,图表会主动采样,不用手动调用接口。

曲线形状怪异通常是你跑的时间太短,只跑 5 个时间单位,曲线自然还没有收敛。谢林模型至少跑 50 个时间单位才能看到分区端倪,100 到 200 个时间单位后基本稳定。你在参数实验时把 "Stop time" 设为 200 就足够了。

5.5 输出结果与论文里的曲线对不上

如果你把跑出来的平均同类比例曲线和经典文献里的图对比,发现数值偏高或偏低,先不要怀疑公式写错。可能的原因有三:

  • 你用的是"连续空间+半径邻居",文献里用的是"网格+摩尔邻居",邻居数量不同,平均同类比例也会不同;
  • 你的搬迁策略是"择优迁移",文献可能用的是"随机迁移",二者的收敛深度不一样;
  • 你的初始分布不是均匀随机,而是有偏移或聚集的,这会让最终隔离值偏大或偏小。

我自己的经验是,忠于原始模型的定义很重要。如果要复现谢林原文的结果,就尽量用摩尔邻居、完全不理性随机搬家这两个设定;如果想扩展模型,再逐步加"择优迁移""异质性偏好"之类的新机制。做仿真最忌一上来就加太多自定义规则,最后说不清现象到底是由哪条规则引起的。

6. 从谢林模型到更多扩展:后续还能做什么

6.1 引入异质性偏好

最初的模型里所有Agent的阈值都是同一个值。真实的社区里,不同人群的包容程度差别很大。可以让每个Agent的阈值在初始化时从某个分布里抽样,比如正态分布,中心值 0.3,标准差 0.1,截断在 [0, 1] 之间。这样模型立刻多出很多行为层次:有的人极其敏感,看到周边少一个同类就搬;有的人比较粗线条,哪怕周围全是异类也懒得动。

加了异质性后,收敛速度会变慢,最终模式也会变成"多个不同规模的小区共存",而不是简单的一块红一块蓝。这个扩展非常适合做课程设计,且不需要额外图形和动画,代码改动不超过十行。

6.2 加入经济因素与地理因素

经典谢林模型是完全空间均质的——每个点的"房价"或"宜居程度"都相同,居民搬家只看邻居构成。这显然不现实。可以给地图设置一块"房价较高"的区域,或者设置一条天然河流、主干道作为不易穿越的地理边界。

在 AnyLogic 里,实现这个的方式是用道路网络空间替换连续空间。把居民放在道路网络的节点或路段上,设置出行距离成本函数。搬家的目标位置必须能通过路网到达,并且距离不能超过某个上限。这样模型就更像城市模拟,能用来讨论"轨道交通改善之后,居住隔离如何变化"这类话题。

不过是的,路网模型比连续空间模型复杂度上一个台阶,建议先把连续空间版跑得得心应手了再碰。

6.3 与他人协作与模型复用

AnyLogic 的单机模型也能做成"云实验"格式,但免费许可一般不支持发布云模型,所以这里不展开。更实际的用法是将模型另存为快照,或者直接发布为 Java 应用,让别人在没有 AnyLogic 的情况下用命令行跑仿真输出结果。

当你把整个模型文件提交给他人时,记得把随机种子、参数值、初始人数这些核心常数都集中放在一个自定义参数组里,别散落到代码里。这样别人拿到模型后,改动参数进行实验比对非常方便。这也是从业人员和初学者之间的一个明显区别:代码能跑起来只是第一步,模型的可维护性、可解释性和可复用性,才是让仿真真正创造长期价值的环节。

按我个人的习惯,在完成任何一个 ABM 模型之后,都会额外花差不多三分之一的时间去整理参数命名、画逻辑流程图、补充注释和边界条件说明。谢林模型虽然逻辑简单,但作为样板工程来打磨,能从中学到的东西远远超出"跑通动画"本身。仿真模型最大的坑永远不是语法,而是你以为自己掌握了系统的因果链条,其实只是被随机性喂了一个巧合的结果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询