多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

NetLogo编程零基础入门:掌握核心概念与ask指令,轻松上手社会网络仿真

NetLogo编程零基础入门:掌握核心概念与ask指令,轻松上手社会网络仿真 先问一件事你是因为觉得编程两个字太吓人才一直没敢打开NetLogo的代码编辑器吗社交背景做研究的人都容易有这种心理门槛总觉得写代码是计算机系的事情。但NetLogo这套社会网络仿真软件最反直觉的地方就在于它恰恰是编程语言里最不挑出身的那一个——建模思路接近自然语言表达逻辑也不是传统的从第一行执行到最后一行而是不断被激活再激活。作为这个系列的第四篇这期不聊NetLogo能做什么只解决一个最现实的问题从零上手NetLogo编程到底该怎么学。先说结论NetLogo的编程基础并不需要你去啃厚厚一本教程。你真正需要掌握的核心不过是几个概念和规则剩下的完全可以靠模仿、查字典、跑通案例来渐进搞定。1. NetLogo的代码长什么样先建立读代码的安全感很多人的第一个心理障碍是没见过NetLogo代码不知道从何下手。NetLogo是Logo语言家族的后代所以它的语法体系特别亲民——代码是由一条条词组成的单词之间用空格隔开每个词都像个英文小短语。换句话说只要你的英语能认出go、turn、forward、red读到NetLogo代码基本就能猜个七八分。这门语言的设计哲学从诞生第一天就不是为了折磨人而是为了让你把精力放在研究问题上而不是语法解析上。1.1 一段能跑起来的代码变量声明过程定义随便给你一段完整的NetLogo示例你就知道它的长相了turtles-own [ energy target-node ] to setup clear-all create-turtles 50 [ setxy random-xcor random-ycor set color one-of [red blue green] set shape person ] reset-ticks end to go ask turtles [ rt random 90 lt random 90 fd 1 set energy energy - 0.01 ] end代码里最关键的信息其实一眼就能看出来开头几行在声明后面to ... end围起来的是各个行为模块。NetLogo不是一行一行顺序讲完整本故事的那种语言它更像个调度中心——setup被按下时执行一次go被按下时一遍一遍地重复执行每次都让所有小海龟做点事情。这和你熟悉的Word、Excel完全不是一个逻辑更像是在给居民楼下的快递柜设定规则哪些柜子有电变量声明什么时候开箱取货setup什么时候用户来存取件go。1.2 别被英文术语劝退NetLogo的词汇量其实很小很多教程会在一开始就抛出大把抽象名词agent、observer、turtle、patch、link、model、widget。看起来很唬人但拿社会网络仿真来说你日常用的词汇其实不超过30个。它们的含义几乎都能在生活里找到对应——turtle是模型里的个体patches是地面格子links是个体之间的连边observer是站在天花板看全局的上帝视角。你不需要背词典只要在一次次调试里让这些词和自己的场景挂上钩它们自然就内化了。我把NetLogo入门需要认识的词汇分成了四组按优先级排列词汇类型例子作用命令类create-turtlesasksetfd让世界发生改变声明类turtles-ownglobalslinks-own给个体或全局增加属性抽屉报告器类countrandomone-ofwith问世界要数据流程控制类ifwhilerepeatforeach让逻辑产生分支与循环慢慢你会发现几乎所有模型底层不过是在用这些词拼出你的研究假设而已。我在给学生们做培训时最喜欢说一句话NetLogo代码是拿来读给朋友听的不是拿来给机器膜拜的。2. 变量、命令、代理编程基础里真正躲不开的三个核心NetLogo代码里最常用的零件并不复杂但它们每一个都像乐高积木里的基本颗粒组合规则值得单独拉出来讲。我建议你在理解代码前先搞清这几个概念到底在说什么。2.1 变量每个代理身上都贴着几个小标签你在NetLogo里新建的每只海龟、每条链接甚至每一块patch都可以被贴上若干个标签。比如做社会网络仿真时你可能给每个节点标记意见倾向、年龄、是否接种过谣言、所在的社区编号。这些标签就是变量。NetLogo的变量和Python、C里的变量有个显著不同它不一定属于整个模型而是可以属于每一个具体的个体。所以当你在代码里写下turtles-own [ age ]其实是给每一只海龟都加了一个叫age的小格子这只海龟的age是5那只可能是42互不干扰。这一下就要区分三种抽屉了globals整个世界共用一个变量比如全局的当前时间步、感染人数。turtles-own / patches-own / links-own属于某一类个体的变量每只各自独立存一份。let/set临时变量和修改变量在过程中生成、修改。做个简单类比全球变量就像学校广播通知里的内容全校只有一套turtles-own就像每个学生的书包每个学生都有一份里面装的东西各自不同let临时变量就像你在草稿纸上随手算的数用完就扔不属于任何学生。2.2 命令一次操作一句话词序影响感受NetLogo整个语法中频繁出现的其实是动词。create-turtles 10是创建10只海龟forward 5是向前走5个单位set color red是把颜色设为红色。每个动词配上一个参数组合成一句对小海龟下的命令。比如写流程时我经常用ask turtles with [color red] [ forward 10 set color blue ]这一段意思简明得很让所有红色的海龟往前走10步然后把颜色改成蓝色。和英语句子几乎同构唯一需要注意的是NetLogo命令的目标通常在前半句或后半句而动作动词写在方括号内部这个后面会讲。习惯读英文的人很容易上手。2.3 过程和调用把事情拆成一个一个能复用的动作如果只有一个setup按钮一直做所有事情代码很快就膨胀到没法维护。NetLogo提供了to ... end结构来定义过程——这个在编程里通常叫函数或方法在NetLogo里叫过程。to initialize-trust ask turtles [ set trust random-float 1.0 ] end定义好的过程你可以随时调用直接在别的过程里写它的名字或是在界面上建一个按钮指定它调用哪个过程。这样设计带来的好处是逻辑拆成小块每块一个任务调试时可以单独跑某个过程看效果你可以先写骨架再逐步填充。写代码写得多的程序员往往会忍不住把一堆东西塞进一个go过程但真正值得模仿的NetLogo老手都会把过程拆得很碎——像一个大公司分成多个部门各司其职。这个过程组织能力在编程基础里占比极高。3. ask指令NetLogo和常规编程最大的分水岭你要是以前碰过Python、C或者Java一定已经养成了一种思维想让一个对象动就调用这个对象的方法比如person.walk()。但在NetLogo里思想的主动权反过来了——你不再以单个对象为主体写代码而是以世界调度员的身份给一群主体下指令。这个指令就是ask。3.1 ask的本质你是在下达群发通知NetLogo中最重要的一类命令几乎每个模型里都会见到ask turtles [ ... ]这行代码的意思是让世界管理所有海龟挨个不保证顺序对每只海龟执行方括号里的动作。仔细体会一下这和我们习惯的面向对象思维完全不同。你不需要找到某只特定的海龟来执行动作只需要说所有满足某个条件的海龟都按这个规则动一下。它非常像微信群里发群公告你不用走到每一个人跟前通知只需要对管理员说ask群成员他们就能收到消息。这种行为模式直接契合社会网络仿真的需求——你不关心某个人单独的动态你关心的是广大人群在规则下呈现的宏观规律。要统计感染比例时你可以写set infected-count count turtles with [infected? true]要模拟传播时ask turtles with [infected? true] [ ask link-neighbors with [not infected?] [ if random-float 1 transmission-probability [ set infected? true ] ] ]读一遍这段代码让所有被感染的个体问一问他们通过连边联系到的那些还没感染的邻居以某个概率决定是否也感染。这几乎就是直接把社会学假设转换成代码中间几乎不需要计算机化的思维跳跃。3.2 ask的执行机制无限循环里的随机轮盘当一个按钮反复触发go过程的时候NetLogo世界其实是在跑一个高频率的循环。每次循环ask turtles [...]会对所有海龟逐一下达指令但具体从谁开始、谁先谁后NetLogo不保证。这个特性在仿真里其实很重要。真实社会中不存在严格的按编号排队行动的现象人们同时在做决策NetLogo的随机轮转恰好模拟了异步并行决策带来的随机性。如果初学者想让某个颜色名单内的海龟先走再让另一群后走就不能指望默认顺序需要自己写顺序控制。比如想让先感染者活动后易感者活动你得显式分两步ask turtles with [infected? true] [ spread ] ask turtles with [not infected? true] [ move-average ]编程新手最容易踩的第一个大坑是写出这样一段代码ask turtles [ if count turtles 50 [ die ] ]这里表面是在检查全局海龟数量但每只海龟被轮流问到时都可能处死别人。数量确实是动态变化的但问题在于这个判断对每只海龟都会重新执行一遍结果是可能最后数量远低于50。初学者期待超过50就杀一批直到等于50但实际上每次都要杀掉一只直到不足50为止这带来极大的波动。正确写法在下一节讲。3.3 self和myself嵌套ask里最容易绕晕的两个我ask进行嵌套时初学者很容易迷失此刻说话的是谁。NetLogo提供两个特殊代词self指出当前正在执行动作的主体最内层asked对象myself指外层ask的那个主体。一个典型场景让每条链接报告两端的节点是谁。如果你在网络模型里写ask links [ show end1 ask end1 [ show self show myself ] ]在ask end1块内self是那个端点海龟myself是当前执行循环的链接。如果你不区分这两个很可能在嵌套环境里误取了错误的代理。做社会网络仿真尤其需要这个——因为每条边连接两个节点你常常需要从边的视角分别让两个端点做点什么。注意NetLogo里的show命令会在命令行控制台打印结果是检查模型逻辑最好的帮手不熟悉的话可以直接用它来输出实验数据。4. agentset、列表和基于主体的思考当你说所有红色的海龟NetLogo返给你的并不是一个列表而是一个叫agentset的东西。agentset和列表是NetLogo建模时存放数据的两种基本容器但它们的性格完全不同。4.1 agentset无序的集合讲究过滤和随机agentset代表一组代理海龟、链接或patch它有几个特点同一类代理才能成团里面不会出现重复的元素元素顺序不确定每次遍历顺序都可能变化支持用with做条件过滤。举例找出度数超过10且意见倾向为支持的节点群ask turtles with [count my-links 10 and opinion 0.5] [ set influential? true ]写多了之后你会发现agentset最适合放进ask和count里这是社会网络模型的高频操作。每次运行模型时agentset都是实时从当前世界计算出来的所以你不用担心它过期——每次使用前它都会重新数人头。agent set基于主体思维有一个很自然的建模效果你不需要手动维护一张谁属于哪个组的表。这个组只是一个临时计算出来的结果。社会网络研究里要圈定某人的二度人脉所有与感染者直接接触的人用agentset一行代码就搞定了。4.2 list有序列表请按索引访问列表就传统的多从头到尾排序允许有重复可以用item、length、map、filter等操作。在社会网络仿真中如果你要存储的是某个节点的属性历史、多轮博弈的策略记录列表是最适合的。每个节点维护一个自己的list可以在每轮结束后往里面加一个值turtles-own [ opinion-history ] to update-history set opinion-history lput opinion opinion-history end这样每跑一个时间步意见历史就被追加进列表。做后续的分析、画时间序列图时直接从列表里取值就行。要注意agentset和list不能互换agentset适合做按照条件批量行动的场景list适合做存储保持顺序的数据的场景。初学者最容易犯的错误是试图对agentset做排序或索引但agentset压根没有index这种概念——它的天然操作是遍历、过滤、随机取样。如果非要给节点排名并取前10你得先把agentset转成list用sort排序。let degree-list sort turtles ; 注意degree-list的元素是排过序的海龟而不是它们的度数如需按度数排序则要自定义sort-by4.3 while循环和无名状态谁在无限占用沙箱编程基础绕不开循环。NetLogo入门阶段主要有三种循环工具repeat适合已知次数的简单重复while适合不知道几次结束的条件循环foreach和map等则用来遍历列表。三种循环里最容易出事的是while——它需要你在循环内部修改终止条件涉及到的变量否则就死循环。我见过好几个人写网络模型时这样写let target one-of turtles while [count [link-neighbors] of target 3] [ ask target [ create-link-with one-of other turtles ] ]初看觉得没问题选一个节点给它的链接数补到3以上。但问题出在如果世界里的其他节点没有链接对象或者它已经连接了所有能连的节点那每次判断还是少于3循环永远不会结束。NetLogo界面会卡死只能强制终止。调试方法其实也简单设置最大迭代次数或者跑之前用if判断一下是否有可行连接对象。5. 实操从零写一个最小社会网络传播模型前面讲了各种概念但只有真正写出一个能跑的模型编程基础才算落地。下面带你走完一个创建节点—生成网络—传播动态的最简单流程。这个模型叫最小观点感染模型里面包含节点初始化、连边生成、一轮传播和可视化布局四个部分。5.1 第一步声明全局变量和主体属性首先定义模型要追踪的数据节点持有什么观点0到1之间的值、是否已经亮明观点这里用一个布尔值announced?模拟受暗示的个体、还有一个标志用来控制传播状态。globals [ consensus-index ] turtles-own [ opinion announced? ]globals里的consensus-index用于衡量整个网络的共识程度——比如把所有人的观点取平均或方差。后续如果做对照实验这个全局指标会很有用。5.2 第二步setup过程——清空世界并创造节点写setup时无论多简单的模型总原则都是先clear-all清空上一次的运行残留。然后设定网络节点的总数创建它们并按圆圈布局摆放。NetLogo里的layout-circle指令可以将所有turtle均匀放在一个圆周上画出来初看像个圆桌会议适合呈现社会网络结构。to setup clear-all create-turtles num-nodes [ set shape circle set color white set announced? false set opinion random-float 1.0 ] layout-circle sort turtles max-pycor - 1 create-network reset-ticks end上面用到了num-nodes这是一个接口上的滑块变量——如果界面上建一个叫num-nodes的滑动条NetLogo会自动把它的当前值代入代码。编辑器里也会显示成绿色代表全局界面变量这个机制非常方便做参数扫描。5.3 第三步生成随机网络连边社会网络仿真中连边结构决定了传播的结果。这里先用最简单的方法每个节点总是连接它的两个随机邻居保证每个人至少和两个人建立起联系。to create-network ask turtles [ repeat initial-degree [ let candidate one-of other turtles if candidate ! self and not link-neighbor? candidate [ create-link-with candidate ] ] ] end逻辑是每只海龟尝试建立initial-degree条不重复的链接。为什么要写candidate ! self和not link-neighbor?因为如果去掉这两个条件你很可能创建出自己连自己的环NetLogo会报错因为链接的两端不能是同一个节点或重复连同一个人导致度分布与预期不符。这两个判断虽然初看像多余的保险丝但在复杂模型里是必备的防抖设计。所有链接创建后可以再用NetLogo自带的layout-spring把网络铺开成一个更有社会网络味道的图。如果想让网络聚合度接近真实社群可以试试layout-ring或layout-tutte差别很大。我这里只做极小网络用人工圆排布就很干净。5.4 第四步模拟传播——每轮做什么事情这里的传播规则设计为每个尚未宣布观点的节点观察自己的邻居中有多少个已经宣布了观点算出它们的平均意见值然后有一小部分概率被说服把自己的意见调整为平均意见。to go ask turtles with [not announced?] [ let informed-neighbors link-neighbors with [announced?] if count informed-neighbors 0 [ let mean-opinion mean [opinion] of informed-neighbors if random-float 1 transmission-prob [ set opinion mean-opinion set announced? true ] ] ] update-visuals tick end这段代码的妙处在于每个个体都没有全局视野它只看自己的局部邻居但最后表现出来的却可能是整个网络的观念趋于一致或两极分化这正是复杂系统研究中自组织特征的体现。仿真时你只要在界面上放一个go按钮一直点下去观察意见的变化趋势就能直观感受到社会网络从个体规则到群体现象的涌现逻辑。5.5 第五步可视化与状态更新程序基础和纯逻辑的重要差别在于你是否能看见模型的动态。常常有模型跑得飞快但控制台里没有任何反馈你根本不知道它跑出了什么规律。所以每次状态更新时我建议顺手给海龟上色意见值越高的颜色越深已经宣布观点的改变形状让画面变得可读。to update-visuals ask turtles [ set color scale-color red opinion 0 1 if announced? [ set size 1.6 ] ] endscale-color是NetLogo的一个隐藏宝藏它把你给的一段数值范围映射到一个颜色梯度上0到1之间的意见值被红白渐变呈现。到这一步你已经用自己的代码写出了一个能运行的传播模型。注意运行时记得先用setup初始化再按go否则会报asking no such agent或跑出一个空世界。如果你在界面上没建transmission-prob和num-nodes两个控件NetLogo会直接提示Nothing named transmission-prob has been defined这时只要去界面页拖两个滑条出来即可。6. 报错与调试被英文提示吓退前先看这里任何一个NetLogo新手初学编程最容易挂在报错看不懂这个环节上。编辑器会突然弹出一行英文按钮失灵世界冻结。编程经验说到底有一大半是跟报错共处的经验NetLogo也不例外。不过NetLogo的报错信息相对友好绝大多数问题就集中在几个模式里。6.1 高频报错信息大词典真正常见的只有六种粗列一下我至今遇到的不夸张说90%的报错都能归进这几类报错信息示例真实原因快速解决办法Nothing named X has been defined引用了一个不存在的变量/过程/滑条检查是否忘了声明或界面上没有对应控件expected command语法解析懵了比如少了方括号检查代码有没有括号没闭合ASK expected this to be a turtle or linkask的宾语不是代理或代理集合确认ask后面跟的是turtles不是colorsCant find element X of a list索引越界用length先量长度Cant set turtle variable X to non-number值域不对检查传入参数BREAK模型无响应代码陷入死循环在循环变量里限制最大步数网格中最容易迷惑的是Nothing named错误。因为NetLogo界面上的滑条、开关也算全局名字你如果没建好控件但代码里引用了它也会报这个错。所以做模型时的顺序应该是先拖动滑条控件给它命名再去代码编辑器里写过程。6.2 调试三板斧show、watch和逐步执行NetLogo的代码编辑器里可以直接运行一行代码。调试时最朴素又最有效的方法是用show在命令行打印某个变量的值。to go show count turtles show count links show mean [opinion] of turtles ... end如果你输出的是每个时间步的关键指标就能监控模型是否按照预期演化。比如打印传播节点总数如果一直是0说明规则条件里可能有逻辑死角如果数量跳变过大说明概率或选择逻辑可能有问题。第二个是界面上的watch功能选择一只特定的海龟视角跟随它等于盯着某个个体追踪它每步的决策。在社会网络模型中我经常在模型运行时用鼠标点击某个重要节点选择watch它看它的意见值在邻居压力下如何变化。这比光看整张图的颜色变化要精细得多。第三个是界面上按钮的慢速运行入口把速度条拉到最慢模型会一帧一帧地跑。遇到想理解某个状态为什么会出现时把速度降下来仔细观察出现变化的那几步往往比读代码更快定位问题。6.3 常见错误案例网络传播模型的三个bug尽管报错解释很直白但很多初学者还是会栽在一些逻辑正确但行为诡异的bug上。我复盘了三个我见过最多次的坑也都发生在我们这类社会网络模型里。第一个忘了用ask指向具体范围直接在observer上下文中调用了set opinion。这样NetLogo会提示you cant set turtle variable opinion of the observer含义是整个世界没有一个人类个体概念你只能通过agent访问属性。所有修改主体属性的操作都必须发生在一个包含多主体的环境里要么ask turtles要么让turtle自己后面的代码块。第二个生成网络连边时只写create-link-with没有写self ! candidate结果某些海龟和自己建了边导致后面传播逻辑出现异常——比如在link-neighbors中遇到邻居是自己。可能NetLogo未必报错但统计出来的指标就会严重失真。所以造网络前老练的人都会在代码里先声明不与自身连边、不重复连边这两个规则。第三个你在ask迭代agentset的同时删除了link-neighbors中的某个个体导致传递过程出现奇怪的行为。NetLogo中ask的agentset是快照式的在迭代过程中删除或修改其中的个体不会导致遍历中断但极容易留下悬空引用。7. 编程习惯真正拉开仿真能力差距的往往不是语法完成了最小模型你已经具备了用NetLogo写基础逻辑的能力。但如果你打算长期用社会网络仿真做研究最后一步我要劝你把眼光从语法挪到习惯上来。很多人在入门之后长时间停留在只会改别人代码的水平问题不是语言没有学会而是缺乏一套属于自己的代码组织和检查流程。7.1 注释不是写给电脑看的是写给你自己看的NetLogo代码里使用分号开头的注释; 初始化环境每次运行前务必执行 to setup clear-all end许多新手觉得代码能跑就行注释浪费时间这在仿真代码里是危险的。因为仿真项目通常要持续一两周你很可能三天后就不记得当初为什么把某个概率设成0.3而不是0.5。每段有研究意义的过程初始化或传播规则都应该用注释说明这个规则的社会学含义。这样既方便自己后期调参也让合作者能读懂你的假设。7.2 开始写代码前先给模型画一张逻辑草图NetLogo学习中最容易被低估的是从脑子里的研究假设到可运行模型之间的过渡。很多卡壳并不是因为代码难写而是因为研究者根本还没有想清楚自己的模型包含哪些主体、哪些规则、怎么让时间步前进、输出哪些指标。我的习惯是在打开NetLogo之前先在白纸上画一个非常草率的流程图或思维导图标出主体是谁个体是海龟群体是patch关系是link每只海龟有哪些属性每个时间步谁做什么决策决策的依据是什么模型需要输出哪些数据模型里有哪些参数需要你来调一旦这张草图画清楚了写代码的过程就变成翻译把所有中文描述翻译成一个一个ask和if。这套方法能让仿真项目整体进度快两倍以上因为它从根本上减少了调试阶段的反复。7.3 不要一次性写完大过程拆小过程、逐步验证NetLogo模型的代码总量通常不会太大但结构乱起来是完全可能的。最典型的坏结构是把所有逻辑堆在一个大go过程里每一步做许多事结果一报错还得一行一行排查。而行家的写法是把go框架搭好里面拆成一个一个小过程每个负责极少的明确任务to go move-agents social-influence update-global-variables plot-trends tick end每写一个小过程立刻用一个最小参数跑一遍确认这个过程没问题再写下一个。这样绝大多数错误会在第一时间被定位到而不至于最后模型死活跑不起来你还找不到是哪一段惹的祸。7.4 用BehaviorSpace批量跑实验前先留下你的“基线版本”如果你做社会网络仿真已经走到要用BehaviorSpace做参数扫描那一步记住一个差点被我忽略的教训做大规模参数实验之前一定要先保存一份基线模型版本。我在做一系列对照组实验时曾为了调试一个附加规则把核心代码改了又改结果一个保存不及时原本的基线模型被覆盖了。后来的实验结论缺少基线不得不花一个下午重新调出原来的核心传播规则重建模型。NetLogo保存模型使用.nlogo后缀文件界面还支持导出模型快照等。你在每完成一个功能模块时就应该另存一个版本文件文件名里加上日期或版本号。坚持这样做才能确保每一次假设调整的结果都可回溯、可对比。这也是社会网络仿真项目是否能发表出严谨结论的隐形分水岭。最后说几句把话留给真正跑过模型的人编程这事儿确实没有捷径但NetLogo的学习曲线已经是同行里最友善的那条了。从今天开始你不需要再因为别人能写代码就觉得自己做仿真研究差一截。对我来说真正有价值的NetLogo经验是从一次次异常报错、一次次变量设错、一次次不收敛模型中长出来的。如果你才刚开始建议你把自己第一个能跑通的最小模型留着一个月后再看你会猛然发现自己已经能看出当时的代码哪里笨拙、哪里有改进空间。祝仿真顺利。
返回列表