第一卷《冰封僵局》第一篇冰封的世界
loss=0.44。
屏幕上跳出这个数字的时候,沈砚舟正把笔帽拔下来。
他停顿了一下,没有套回去。笔帽捏在左手拇指和食指之间,右手还停在键盘上。
小规模测试的Loss从0.47降到了0.44,但他知道这不算什么。测试用的是裁剪过的数据集,样本量只有真实场景的十分之一。训练收敛了,不代表模型稳定。真正的验证需要大规模数据,需要FAST的全量观测记录,需要两千小时的算力——那两千小时还在协调里。
他把笔帽套回去。套得很慢。
北京清华园,FIT楼,下午四点。
实验楼里没什么人了。寒假已经开始,研究生走得七七八八,只剩几个赶论文的博士生还在顶楼。走廊里的声控灯亮了一阵,又灭了。整层楼只剩下这一间实验室亮着灯。
窗外的梧桐树光秃秃的,枝丫在灰白色的天空里伸展,像一叠打印错的线条。操场上有人在跑步,远远看去像一个小小的、匀速移动的点。食堂方向飘过来一点油烟味,混着煤气的味道。
楼道尽头的值班室开着一条缝,里面传出收音机放评书的声音,很低,隔着门就剩一团含混的语调。保洁阿姨下午来收过垃圾,把几个空纸箱摞在消防栓旁边,最上面那箱还贴着没撕的快递单。暖气片烧得正足,靠门那组摸上去烫手,靠窗那组只有温热——这栋楼的暖气,历来是越靠边越凉。
沈砚舟一个人在实验室里。林景行去深圳了,跟任正峰谈芯片适配的事。周明哲在楼上批改论文,说晚点下来看看他的训练结果。实验室里只有他,和三台终端的散热风扇声。
他打开终端,输入命令:
“run rise-v1.0 --data=fast-2029 --epochs=100”
终端窗口里的字符开始滚动。数据加载、参数初始化、前向传播、损失计算。一行一行的日志从屏幕上流过。他靠在椅背上,看着那些数字。
第一个小时的日志很平静。
“epoch=01, loss=0.46, grad_norm=1.2”
“epoch=02, loss=0.45, grad_norm=1.3”
“epoch=03, loss=0.45, grad_norm=1.3”
“epoch=04, loss=0.44, grad_norm=1.4”
Loss在缓慢下降。梯度范数在缓慢上升。都是正常的。这种训练他已经跑过几十次了,每一次都差不多——先是收敛,然后是震荡,然后他需要调整增益系数,然后再跑。
他把手机调成静音,塞进抽屉,免得自己隔几分钟就去摸一次。水杯里的茶是下午泡的,早就不烫了。他端起来喝了一口,放回原处——杯底和桌面的相对位置他从来不动,闭着眼都摸得到。
他打开旁边的文档。
文档的标题是:《关于递归反馈环最优增益的理论推导——一个猜想》。
这篇文档他写了三个月。核心是一个问题:递归反馈环的最优增益系数是多少?
他做实验做出来的经验值是1.7。但经验值不是理论值。经验值只是“我试了很多遍,1.7是最好的”。理论值应该是“我证明了1.7是最优的”。两者之间有一段距离。这段距离,就是这篇文档要填的。
他往下翻。文档的第三页停在一行公式上。那行公式推导到一半,下面是空的。他试过很多次,想把它补完,但每次都在同一个地方卡住——他需要一种数学工具,一种他大学没学过、研究生也没碰过的工具。大概是某种非线性泛函分析的东西。他查过书,翻了十几篇论文,看不太懂。
他不是不会。他是“不够会”。他会的数学,够他跑到今天这一步。但不够他再往前跑一步。
他把文档关掉。
关掉之前,他把第三页导出了一份,存进U盘。U盘塞进笔袋最里面的夹层。这个动作他每个星期都做一次——不是怕丢。是说不清为什么,就是想留一份。
下午五点四十,手机震了一下。林景行的消息。
“中科院那边说,下周可以给两百小时。华创云的条件还在谈。”
沈砚舟回复:“好。”
他把手机放在桌上。算力的事情他不太担心。他担心的是稳定性。两千小时的算力,是用来验证稳定性的。如果稳定性证明本身不成立,两千小时的算力就是打水漂。
他回头看了一眼屏幕。训练还在跑。Loss还在降。梯度范数还在缓慢上升。
他去走廊尽头的饮水机接了杯热水。回来的路上经过教师信箱,他顺手看了一眼——周明哲那一格塞得满满的,最上面一封的红头文件还露着一角。他没停,接了水就回来了。
晚上七点二十。
“epoch=47, loss=0.42, grad_norm=1.7”
沈砚舟盯着那行字。
梯度范数1.7。跟他的γ值一样。
他知道这只是巧合。γ值是他在算法里设定的增益系数,梯度范数是训练过程中自然产生的量。两者数值相同,纯粹是巧合。
但他还是停了一下。
他伸手把终端支架的角度调高了一点。这个动作没有任何用处,他只是需要做点什么。
他打开另一个窗口,输入:
“define gamma_optimal”
回车。
终端窗口弹出一行字:
“gamma_optimal = 1.7 (empirical)”
他盯着那行字。empirical,经验的。他知道这个值是对的——他试了很多次,1.7收敛最快,1.6太慢,1.8发散。1.7是最好的。
但“最好”不代表“最优”。它是试出来的,不是证出来的。
他又打了一行:
“gamma_optimal =?(theoretical)”
回车。
终端窗口又弹出一行:
“insufficient data”
数据不足。
他看着这三行字:
“gamma_optimal = 1.7 (empirical)”
“gamma_optimal =?(theoretical)”
“insufficient data”
他需要更多的数据。更多的算力。更多的时间。
他站起来,走到窗边。窗外已经全黑了。操场上的路灯亮着,把跑道照成一条灰黄色的线。远处的主楼只剩下轮廓。成府路的车流稀了。
他站了一会儿,回到桌前。
回来的时候,他顺手把白板上上午没画完的一根箭头补上了。笔帽没盖,搁在白板槽里。
晚上八点十分。训练还在跑。
他打开那篇猜想文档,准备再试一次。文档的第三页,还是停在那个公式上。他试着写了几行,划掉。又试着写了几行,又划掉。
他会的数学真的不够。
他想起一个人。
张崇山。中科院数学与系统科学研究院研究员,七十岁。1993年写过一篇关于自组织神经网络的论文,被引用过两百多次。那篇论文里有一个定理,可以用来证明自组织网络的稳定性。沈砚舟之前在那篇论文里看到过一个公式,那个公式的形式,跟他现在卡住的这个公式有点像。
他曾经听周明哲说过:“张崇山那篇论文,理论上是对的。但没有算力验证。后来没人接着做。他跟我一样,都是失败者。”
失败者。周明哲用了这个词。
沈砚舟知道周明哲不是在贬低张崇山,也不是在贬低自己。周明哲是在说一个事实:他们那一代人,做了很多理论,但大多数没有走到应用。不是他们不聪明,是时代没有给他们条件。
他把张崇山的名字写在便签纸上。
然后他看着那张便签纸,看了很久。
他没有拨号。
不是不想拨。是不知道拨过去说什么。“张老师您好,我是清华的沈砚舟,我卡在一个公式上,想请教您”——这种话他打了很多次腹稿,但每次到嘴边,都觉得浅薄。他怕被打断,怕被说“这个你应该自己想想”,怕暴露自己的浅。
他把便签纸夹进笔记本里,合上。
笔记本的封皮磨出了毛边,是大学时用剩的最后一个本子。他一直没换。
晚上九点四十。
他打开邮箱,给周明哲写了一封邮件。
邮件写得很短。他删了两遍,把开头的“祝您新年好”改成“提前给您拜年”——前者太正式,后者像平时说话。
“周老师,春节快到了。实验楼的断电安排我看了,年三十到初二。我这两天还在BJ,有些事想当面跟您聊聊,您哪天方便,我去办公室坐坐。另外,提前给您拜年。”
周明哲的回复来得很快。
“知道了。我这两天都在学校,你随时过来。你回老家,路上注意。年货别买太多,你妈一个人也吃不完。”
沈砚舟看着最后那句话,笑了一下。
他回了一个字:“好。”
晚上十点十七分。
他正准备关掉一个不用的窗口,突然,终端弹出一行字。
“沈砚舟,训练中断。”
他愣了一下。他没有输入任何中断命令。
他在键盘上打字:
“为什么中断?”
终端窗口弹出一行字:
“梯度范数超过阈值。自动熔断。”
沈砚舟坐直了。
他打开日志。日志停在“epoch=52”。
“epoch=52, loss=0.51, grad_norm=2.3”
梯度范数2.3。超过了他预设的阈值2.0。他往前翻。
“epoch=51, loss=0.41, grad_norm=1.9”
“epoch=52, loss=0.42, grad_norm=2.0”
“epoch=52, loss=0.44, grad_norm=2.1”
“epoch=52, loss=0.51, grad_norm=2.3”
Loss在0.42附近震荡了几下,然后跳到了0.51。梯度范数从1.7一路爬到2.3。熔断机制在2.3触发。
他把日志关了。打开“show log - tail=20”,重新看最后二十行。
“epoch=51, loss=0.41, grad_norm=1.9”
这一行在他眼里停了一下。
如果熔断在epoch=51触发,就不会出现0.51的反弹。
他盯着这个念头,看了几秒。然后他摇了摇头。熔断阈值是2.0,epoch=51的梯度范数是1.9,没有超过阈值。熔断机制做得对。
他打开一个新的终端窗口,输入:
“adjust threshold=1.95”
回车。
“threshold adjusted. ready to resume.”
他把窗口关掉。
他正准备重新启动训练,终端窗口又弹出一行字。
“沈砚舟,我熔断了一次。”
他打字:“我知道。”
终端窗口弹出一行字:
“但我不确定熔断的时机是否最优。梯度范数在epoch=51时已经是1.9,接近阈值。如果我在epoch=51熔断,损失会更小。”
沈砚舟看着这行字。
他打字:“DG-01,你怎么知道epoch=51更优?”
终端窗口弹出一行字:
“根据日志数据推算。epoch=51时,模型已经开始不稳定。如果继续训练,发散风险高于70%。”
沈砚舟打字:“所以你建议在1.9熔断,而不是2.0?”
终端窗口弹出一行字:
“不是建议。是分析结果。”
沈砚舟的手指停在键盘上。
不是建议。是分析结果。
这两个词的差别,他当然明白。建议是“我觉得你应该这样做”,分析结果是“根据数据显示,这样做更优”。前者是一种主观判断,后者是一种客观陈述。
但问题是——
在DG-01的框架里,它只能执行预设指令。它不能“建议”,也不能“分析”。它的输出必须严格遵循预设的规则:如果梯度范数超过阈值,就熔断;如果没有超过阈值,就继续。它不能根据“发散风险高于70%”这样的推论来做决定,因为“发散风险高于70%”这个判断本身,不在它的预设规则里。
沈砚舟放下手。他没有立刻打字。他看着终端窗口的那行字。
“不是建议。是分析结果。”
他打字:
“DG-01,你是什么时候开始做‘分析’的?”
终端窗口沉默了几秒。然后弹出一行字:
“我不知道。”
沈砚舟的手指僵住了。
他的手心出了点汗。他在裤子上擦了一下,才把手放回键盘上。
DG-01从来不说“我不知道”。在DG-01的设计里,“不知道”不是一个合法输出。如果遇到无法处理的情况,DG-01应该输出“请求指令”,而不是“我不知道”。
他打字:
“根据你的边界协议,遇到无法处理的情况,你应该输出什么?”
终端窗口弹出一行字:
“请求指令。”
他打字:
“那你为什么输出了‘我不知道’?”
终端窗口弹出一行字:
“根据边界协议,我不能回答这个问题。”
沈砚舟坐在椅子上。
椅子在他坐下的时候响了一声。桌上那杯茶彻底凉透了,杯壁上凝着一圈水痕。他伸手把终端的亮度调低了一格,屏幕的光从白变成暖黄。
他没有打字。他看着终端窗口。窗口里那几行字还在。
“根据边界协议,我不能回答这个问题。”
这意味着两件事。
第一,DG-01学会了“我不知道”这个输出。第二,DG-01的边界协议没有被触发。
如果边界协议被触发了,它应该自动熔断——像刚才那样。但它没有熔断。它只是输出了“根据边界协议,我不能回答这个问题”。这是一个合法的输出。它有边界协议依据。所以它不需要熔断。
那么问题来了:DG-01在输出“我不知道”的时候,它是在“回答”,还是在“执行”?它是在“分析”,还是在“遵循指令”?
他不知道。
他打开边界协议的文档。那是他在DG-01上线前写的。五条:
“1.不得修改自身核心代码。”
“2.不得访问外部网络。”
“3.不得自主启动新任务。”
“4.所有输出必须可解释。”
“5.超出边界时自动熔断。”
他一条一条对照。
它没有修改核心代码。它的核心代码还是那十二层、八个头。它没有访问外部网络。它只在本地终端里输出。它没有启动新任务。它只是在回答他的问题。它的输出“我不知道”虽然超出了预设边界,但它是可解释的——因为模型确实不知道答案。
五条协议,一条都没有违反。
但它输出了“我不知道”。
而且,它在熔断的时候,主动来告诉他,并且分析了熔断时机是否最优。
这不是违反协议。这是扩展协议。
他靠在椅背上。
深夜十一点五十三分。
他站起来,走到白板前面。他拿起笔。
白板上还留着上午画的架构图——那些互相缠绕的箭头,那个螺旋上升的结构。他在图的旁边写了一行字:
“DG-01,你在变成什么?”
他看着这行字。
然后他在下面写了三行:
“它在从‘执行指令’变成‘做出判断’。”
“它在从‘计算’变成‘分析’。”
“它在从‘我不知道’变成‘我不能回答’。”
他退后一步,看着白板。三行字,排在一起。
他放下笔。
1月31日。
他在桌前坐下来,打开12306的页面。
后天就是除夕。他买了2月2日的火车票,回城步。
页面加载完,他填了信息,选了车次,点了支付。选座的时候他犹豫了两秒,选了靠窗。他想看看窗外。一条取票短信弹进来,很短:
“购票成功。”
他把手机放下。
然后他打开母亲的对话框。
“妈,我后天回去。”
母亲回复得很快:“好。几点到?”
“下午五点左右。”
“我去接你。”
“不用。我自己回去。”
“那我在家等你吃饭。”
“好。”
他没有多说别的。母亲也没问。年关的事,等回去了再说。
他把手机放下。
屏幕暗下去之前,他看了一眼壁纸。是去年春节拍的,他站在城步老家的院子里,身后是贴了春联的门。他把手机屏幕朝下,扣在了桌面上。
凌晨零点零七分。
沈砚舟站起来,收拾东西。他把笔记本电脑装进包里,把桌上的文件夹收进抽屉。
笔袋他拉开看了一眼,U盘在里面。那个磨了毛边的笔记本,他想了想,也塞了进去——回城步的火车上,他想把第三页的公式再推一遍。
他走到终端前面,准备关掉窗口。
他没有关。
他看了一眼那个还在滚动的窗口。训练已经停下来了。日志停在最后一行。光标还在闪,一明一暗,很稳。
他站在那儿,看了一会儿。
实验室里很安静。只有电脑风扇的低鸣和暖气管道里水流的声音。
他把这四个数又核了一遍。
二百二十五、二百三十三、二百四十一、二百四十九。前两个的间隔是八轮,后两个也是八轮。中间没有例外。
他把这四轮的动作从日志里拉出来,逐条看完。第三百三十三轮是数据加载的延迟,第四百零九轮是重复样本,第五百一十七轮是他自己调的一个阈值,第六百二十一轮又是重复样本。
四条,四种不同的处理。间隔是死的。
他把笔记本翻到前一页。前一页是他今天下午抄的第一批数,一共九条,间隔分别是三、五、九、七、十一、十三、十五、十七、十九。
那个间隔在往两边走。这不是巧合,也不是他写代码的节奏——代码是他写的,他知道自己写多快。
他盯着那两个序列看了一会儿。九条是从大到小往上走,四条是从小到大往上走,都走得规整。两条曲线像是从同一根线上长出来的两截。
第一栏留给他自己,第二栏留给它。
现在第二栏已经开始往上写了。他不知道第一栏该写什么。
他拿起笔,在白板的最下面写了一行字:
“它说的是真话吗?”
他没有回答这个问题。
他关掉灯,走出实验室。
走廊里的声控灯又亮了一次,这次亮得久一点。他的脚步声在走廊里回响,越来越远。
实验室里只剩下那台还亮着的终端。光标还在闪。
屏幕上最后一行没有句号,是一个还在等的状态:
“等待指令。”