凌晨两点。实验室的灯关了——没关,是走廊的灯关了。实验室里只有电脑屏幕的荧光照着半面墙,墙面的乳胶漆在荧光下泛青,像停尸房的冷色调。
屏幕上代码从最上面的#include到最下面的return 0,滚动条拉了十二下才到底。第十一版了。GA_pipe_v1.11.c。
代码从两百行变到了三千行。
两百行的时候只有Dijkstra——最短路径算法,输入拓扑和权重,输出最短路径。像一把直尺——放上去量,量完告诉你哪条最短。简单、直接、没有选择余地。最短就是最短,没有第二短。
后来Dijkstra不够了。管网不是一条最短路径的问题——管网是多目标优化,要同时考虑建设成本、水压分布、供水可靠性。最短路径只管成本,不管别的。就像只知道找最便宜的路走,不问那条路通不通、堵不堵、安不安全。
所以改遗传算法。遗传算法不是一把直尺——是一个种群。初始化一百条染色体,每条染色体代表一种管径组合。然后选择、交叉、变异——差的淘汰,好的保留,在好的基础上微调。一代一代筛下去,像在河里淘金——把沙子冲走了,金子留下来。
再加选择算子——轮盘赌选择,适应度越高被选中的概率越大。再加交叉算子——两条染色体的基因片段交换位置,像两根管子的管段交换连接点。再加变异算子——随机改变某个基因位上的管径值,像水管的某个节点突然换了一根不同粗细的管。
三个算子写完,代码从两百行膨胀到一千行。又加了约束条件处理——惩罚函数,节点水压不满足最小服务水头的个体适应度降一半,管段流速超标的降三分之一。再加精英保留策略——每代适应度前五的个体不参与交叉和变异,直接进入下一代。像给最好的种子留一块不受打扰的田。
又加了自适应变异率——迭代初期变异率大一些,后期逐步缩小。像挖掘——先大面积挖走表土,再小面积精细筛选。
三千行了。一个程序从两百行长到三千行,文件名还是那个文件名,里面却已经塞进了太多东西:输入、约束、迭代、输出、日志。每多一个功能,就多一段要照看的代码。它不再像最初那样一眼能看到头。
程序在跑。
屏幕上黑底绿字——遗传算法的迭代信息一行一行往上翻,翻得很快,像一个人在快速翻一本很厚的书只看页码。
第800代:适应度 778.1
第900代:适应度 759.3
第1000代:适应度 749.8
第1500代:适应度 721.4
每一次迭代,适应度在降低。降低意味着什么?意味着成本在降低。优化在这个程序里的含义就是——用更少的钱,铺更合理的管。每一代淘汰一批差的个体,保留一批好的个体,好的个体再交叉、变异、产生更好的后代——一代一代,成本越来越低,管径越来越合理,水压越来越均匀。
程序在学。它不知道自己在学——它只是在执行选择、交叉、变异三个算子,一千次、两千次。但结果看起来像是在学:每过一百代,答案好一点。每过五百代,答案好一大截。它没有意识、没有目的、没有方向感。它只是按照我写的规则在做——和我设的规则做的一样。规则说差的淘汰、好的留下,它就留下好的。规则说变异率0.05,它每100个基因位就随机变5个。它不思考为什么要变——它只执行。
和自然选择一样。自然不思考为什么长颈鹿的脖子长——脖子短的够不到高处树叶,饿死了。脖子长的活下来,生下来的小长颈鹿脖子也长。一代一代,脖子越来越长。没有方向——或者说,方向是环境给的。环境说了算:高处有叶子,你可以够到,够到就活得下来。
市场也有一点类似的影子。赵启明说过——有效市场假说:市场永远在给资产定价,定出来的价是所有人博弈之后暂时留下的结果。有人买,有人卖,价格涨了跌了,最后总有一些策略留下来,也总有一些策略被淘汰。
我看着屏幕上的数字继续翻。
第1800代:适应度 708.6
第1900代:适应度 703.1
第2000代:适应度 698.7
降速在放缓。曲线在收敛。像一条河从山里冲出来——刚出山的时候落差大,流速快,冲刷猛;到了平原落差小了,流速慢了,开始打弯。六百多代之前适应度从两千多一个猛子扎到一千以下,现在两千代了,从七百零几磨到六百九十八。六代才降不到两个点。
程序在学。学得很慢了——像一个人在黄昏的院子里一圈圈踱步,每走一圈步子都一样,但他还在走。也许走到天黑就停了,也许走到天亮发现的路径和昨晚不一样。但此刻他还在走。
手里握着蓝色钢笔。钢笔的笔帽裂了一道缝——那天坐在实验室里握得太紧,塑料帽被指甲摁出了一条裂纹。透明胶缠了两圈,胶带的边被汗浸了变黄了,但还粘着。笔没换——不是买不到新笔,是这支笔从初三用到研一,换笔就像换一根管——不是不能换,是不想换。
翻开笔记本。左手边是管网优化的代码笔记,右手边是给排水的理论笔记。两本笔记并排——左边是代码、变量、函数、循环,右边是公式、图表、管径、水压。翻着翻着,某一页上我看到了一行字——是大二那年写的:
"流动。"
就这两个字。下面没有句号。两个字孤零零的,像一截管子的一段切面——切口很齐,但没有接上另一段。
我看着这两个字看了一会儿。然后翻到后面一页空白纸。
蓝色钢笔的笔尖落在纸上。先写了一行:
管网→算法
写完停了一下。然后在箭头后面又写了一个箭头和一个问号:
管网→算法→?
问号后面是空的。
我记得第一次写代码的时候是在高中机房,宋老师给了那张带QBASIC程序的软盘。那时候代码只有十几行——输入、计算、输出。没有分支,没有循环,没有选择。
现在代码有三千行——有选择、有交叉、有变异、有迭代、有评价、有淘汰。输入从一头进去,经过一千次选择和两千次迭代,从另一头出来的不是答案本身,而是一个看起来更接近答案的方案。
如果把管网换成K线图呢?
这个念头从脑子里跳出来的时候,我手里的钢笔停了。笔尖在纸上按压了一下,纸面上出现了一个很小的墨点——蓝色钢笔墨水在白纸上洇成一个微小的圆,圆的边缘是毛的,像水面上的水纹扩散到极限然后停住。
如果把管网换成K线图呢?
节点可以不是节点,管径也可以不是管径。程序真正处理的,是一组变量、一组约束和一个要不断逼近的目标。管网里,目标是成本更低、水压更稳;如果换成市场,目标也许会变成收益、回撤和胜率。
这个念头还很粗,粗得不能写成公式。
我拿钢笔在"算法→?"的问号旁边画了一条线,线的尽头没有箭头——箭头还没有画。因为那个方向我只是看到了,还没有走进去。就像站在一个交叉路口,看到了左边和右边的路标,但脚还站在原地。
程序还在跑。屏幕上的数字还在翻。
第2400代:适应度 670.5
六百七十。比两百代时候的九百八十七降了三百多。程序已经不降太多了一代到三十。曲线在收敛。收敛的意思是——它找到了。找到最优解了。
我把笔记本合上。程序继续跑。我站起来伸了个腰——脊椎在脖子和腰之间咔嚓响了三下。实验室里只有电脑风扇嗡嗡响。
窗外是哈尔滨十二月的夜。零下二十度。窗玻璃上结了一层冰花——冰花是水蒸气在冰冷的玻璃上结晶形成的,六角形的,像微型的管网铺在窗上。冰花的每一根枝从中心往外分叉——和城市给水管网的拓扑一样,从水源到干管到支管到用户。水蒸气不知道自己要长成管网的样子——但物理规律让它在玻璃上走出了和水管相同的路径。
我关掉显示器——不是关机,只是把屏幕黑掉。程序在后台继续跑。走出实验室的时候回头看了一眼——电脑主机的电源灯在黑暗里一闪一闪,绿色的,像一只不会闭的眼睛在呼吸。
走廊很暗。只有安全出口的绿牌亮着,绿光投在水泥地上画出一个方形。安全牌上是一个跑动的小人图标——跑动的人,跑向出口的方向。我的影子被绿光照出两道——一道朝前,一道朝后。两道影子走同一个方向。