ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用AnyLogic实现谢林模型:从微观规则到宏观隔离的智能体仿真

用AnyLogic实现谢林模型:从微观规则到宏观隔离的智能体仿真 几年前第一次在课堂上见到谢林模型的时候我就觉得这东西特别有意思——明明每个人只想在自己的小圈子里待得舒服一点结果整个城市却自发地画出了泾渭分明的一块块区域。这种微观动机和宏观结果之间的巨大落差比很多复杂的热力学公式都来得震撼。最近用 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其中 group1 和 group2 各占一半。你可以直接填写 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 像素填色条件为如果 group1填充红色如果 group2填充蓝色。如果该居民已经满意同组邻居比例达到阈值可以画一个绿色边框如果不满意边框画成灰色。这样在动画里你能一眼看出哪些居民处于即将搬家的状态。这算是仿真建模里很讨巧的做法——把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() ListResident 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 是否真的被触发了。比如事件没设置成循环模式或循环周期设置太长都会造成不搬家的假象。再检查阈值是否设得太低——如果 threshold0那么每个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 为 timeY 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 模型之后都会额外花差不多三分之一的时间去整理参数命名、画逻辑流程图、补充注释和边界条件说明。谢林模型虽然逻辑简单但作为样板工程来打磨能从中学到的东西远远超出跑通动画本身。仿真模型最大的坑永远不是语法而是你以为自己掌握了系统的因果链条其实只是被随机性喂了一个巧合的结果。
返回列表