【波士顿,剑桥市,MIT人工智能实验室】
2016年1月12日,凌晨2点17分
陆星衍按下回车键,屏幕上的代码开始运行。实验室里只有他一个人,其他博士生要么早就回去睡觉了,要么今晚根本没来。白色的LED灯管发出均匀而冷冽的光,照在银灰色的机箱阵列上,那些机器正在嗡嗡低鸣,为他的神经网络训练提供算力。
窗外的波士顿冬夜漆黑如墨,只有远处查尔斯河对岸的哈佛校园还亮着几点灯光。实验室位于MIT斯塔塔中心的三楼,巨大的玻璃窗外能看到校园里光秃秃的树木和积着薄雪的小径。
陆星衍盯着屏幕上滚动的日志:
Epoch 100/1000
Training Loss: 0.437
Validation Loss: 0.598
梯度范数: 0.003
梯度范数太小了。小到几乎可以忽略不计。
他靠在椅背上,闭上眼睛,右手拇指和食指按在鼻梁两侧。这是他从高中就有的习惯,思考难题时会这样做。
梯度消失。深度神经网络训练中最经典也最恼人的问题之一。网络层数太深,反向传播时梯度信息逐层衰减,传到最前面的层时已经近乎为零,导致前面的参数几乎无法更新。
他试过几种方法:批归一化、残差连接、梯度裁剪。都有改善,但都不够彻底。模型在训练集上表现尚可,但在验证集上很快就过拟合了。
他睁开眼睛,从桌上拿起一叠草稿纸。最上面那张已经写满了公式:链式法则的展开,激活函数的导数,权重矩阵的更新规则。
他在空白处写道:“如果是清辞,会用动量优化。”
这不是随意想到的。是高三那年,他们一起准备信息学竞赛时,沈清辞解决过一个类似的问题:递归搜索时,在深层分支中容易迷失方向,找不到最优解。沈清辞当时提出了一个方法:给搜索过程加上“动量”,让它不会在某个局部停滞太久,而是能够跳出陷阱,继续探索。
“就像滑雪,”沈清辞当时解释说,“如果你在某个小坑里停住了,就永远出不来。但如果你有足够的速度,就能冲过去。”
陆星衍记得自己当时的回答:“动量在物理中是惯性,在算法中是历史信息的积累。”
“对!就是这个意思!”
那是2012年的冬天,云城一中的计算机教室里,暖气片发出咝咝的声音,窗外在下小雪。他们挤在一台电脑前,沈清辞的手指在键盘上快速敲击,陆星衍在旁边看着他写代码,偶尔提出修改建议。
四年了。
陆星衍放下笔,看向窗外。波士顿的雪比云城大得多,也更冷。但他记忆里最清晰的,还是云城那种湿冷的冬天,和计算机教室里那台老旧的暖气片。
他重新看向屏幕。动量优化。他试过了,Adam优化器本身就包含动量概念。但也许可以调整参数?或者尝试Nesterov加速梯度?
他打开另一个终端窗口,开始修改代码。手指在键盘上敲击的速度很快——这是他在华清四年训练出来的速度,也是……当年和沈清辞一起写代码时养成的节奏。
他们曾经比赛谁打字更快。沈清辞总是赢,因为他手指更长,而且有种天生的节奏感。但陆星衍的逻辑更严谨,bug更少。
“我们是完美组合,”沈清辞有一次说,“我负责速度,你负责正确性。”
陆星衍当时没说什么,但心里同意。
完美的组合。
但已经四年没有组合了。
【旧金山湾区,斯坦福大学格林图书馆】
2016年1月12日,晚上11点03分
沈清辞盯着屏幕上的递归函数,眉头紧锁。图书馆的这个角落很安静,只有他一个人,还有几排布满灰尘的旧书——这里存放的是1990年代以前的计算科学文献,几乎没人来看。
窗外是斯坦福的夜晚。加州冬天温和得多,没有雪,只有偶尔的细雨。路灯在湿漉漉的路面上投下黄色的光晕。
屏幕上的代码看起来很简单:
```python
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
```
经典的非波那契数列递归实现。问题出在性能:计算fibonacci(40)需要几秒,fibonacci(50)需要几分钟,fibonacci(100)理论上需要……他不想算。
作业要求优化这个函数,使它能快速计算fibonacci(1000)。
沈清辞试过几种方法:记忆化(缓存已计算的结果)、迭代法(用循环代替递归)、矩阵快速幂(数学优化)。但教授的要求是:“展示你对递归本质的理解,而不仅仅是绕过它。”
他靠在椅背上,揉了揉眼睛。从晚上七点坐到现在,四个小时了。桌上的能量棒包装纸和空咖啡杯证明了他的努力。
“阿衍说过这个问题可以用尾递归优化。”
这句话是自然而然出现在脑海里的。不是刻意回忆,而是像条件反射一样跳出来。
高三那年,他们一起准备数学竞赛,有一道题涉及到递归数列。陆星衍在草稿纸上写:“递归的本质是栈,但尾递归可以被编译器优化为循环,避免栈溢出。”
沈清辞当时没完全懂:“尾递归是什么?”
“就是递归调用发生在函数的最后一步,没有后续操作。”陆星衍用他那典型的、清晰的解释方式说,“这样编译器可以复用当前栈帧,而不是创建新的。理论上,尾递归的空间复杂度是O(1)。”
“就像……循环?”
“对,就像循环。但保持了递归的思维模式。”
沈清辞当时觉得这很酷。不是因为他完全理解了尾递归,而是因为陆星衍解释时的样子:专注,耐心,眼睛里有一种分享知识时的微光。
四年了。
沈清辞重新看向屏幕。尾递归。在Python中,尾递归优化并不像函数式语言那样原生支持,但可以手动模拟。或者……可以展示如何将普通递归转换为尾递归形式,然后解释为什么这种形式可以被优化。
他开始写代码。手指在键盘上的速度很快——这是他在斯坦福三年训练出来的速度,也是……当年和陆星衍一起学习时养成的习惯。
他们曾经共用一台电脑写代码。沈清辞负责输入,陆星衍负责审查。沈清辞的手指在键盘上飞舞,陆星衍的目光在屏幕上扫描,偶尔说:“这里,括号不匹配。”或者“这个变量名起得不好。”
“你怎么总是能发现我的小错误?”沈清辞有一次问。
“因为我看得仔细,”陆星衍说,“而且我了解你的代码风格。”
了解你的代码风格。
四年了,他的代码风格变了吗?应该变了。斯坦福教会了他更规范的命名,更清晰的注释,更模块化的结构。
但有些东西没变:他还是喜欢在复杂算法前写一行注释说明核心思路;还是习惯用i、j、k作为循环变量;还是在函数结束时喜欢留一个空行。
如果陆星衍现在看他的代码,还能认出是他的风格吗?
沈清辞写完尾递归版本的fibonacci函数,运行测试。fibonacci(1000)瞬间计算出结果,数字长得屏幕都显示不下。
成功了。
他看着那个巨大的数字,突然感到一阵失落。
成功了,但陆星衍看不到。
【波士顿,凌晨2点48分】
陆星衍的咖啡喝完了。他起身走向实验室角落的咖啡机——那是博士生们集资买的,一台半专业的意式咖啡机,能打奶泡,能做拿铁。
他拿出自己的杯子:MIT纪念杯,红色logo,是入学时学校发的。他从柜子里拿出咖啡豆,研磨,装入手柄,压实,放到机器上。按钮按下,浓缩咖啡缓缓流出,深棕色,表面有一层油脂。
他不需要加糖或奶。从高中起就喝黑咖啡,沈清辞总是说他“自虐”,然后给自己的咖啡加两包糖。
“你这样喝不苦吗?”沈清辞问过。
“苦,但清醒,”陆星衍回答。
“我加糖也能清醒。”
“糖会让人迟钝。”
“胡说!糖分是大脑的能量!”
那是高三的某个晚自习后,他们在学校门口的便利店买咖啡。云城一中的便利店很小,咖啡是速溶的,但对他们来说足够了。他们坐在便利店外的塑料椅上,看着冬天的夜空,喝着廉价的咖啡,讨论着当天的数学题。
陆星衍端着咖啡回到座位。热气升腾,在冷冽的实验室空气里形成白色雾气。
他拿起一支笔,在咖啡杯的纸套上画了一个图案:一颗五角星。很简单,几笔勾勒。
高中时,沈清辞有个奇怪的习惯:在草稿纸角落画小星星。问他为什么,他说:“标记重要的时刻。”
“做数学题算什么重要时刻?”
“每解出一道难题,都是一个重要时刻,”沈清辞认真地说,“都应该被标记。”
后来陆星衍也养成了这个习惯。在重要的草稿纸页,在解出难题的瞬间,画一颗小星星。
四年了,这个习惯还在。
他放下笔,看向屏幕。新的训练开始了,梯度范数有所改善,但还不够。他需要更激进的方法。
也许……可以试试沈清辞当年提出的另一个想法:“分层训练”。
那是一次讨论深度网络时,沈清辞随口说的:“如果一次训练整个网络太难,为什么不一层一层来?先训练第一层,固定它,再训练第二层,以此类推。”
当时陆星衍觉得这想法太简单,不符合深度学习端到端训练的理念。但现在想来,在某些情况下,这种“贪婪分层训练”也许有效,尤其是当梯度传播困难时。
他打开新的代码文件,开始实现分层训练框架。
窗外,波士顿的凌晨三点,雪开始下了。
【旧金山,晚上11点37分】
沈清辞也起身去冲咖啡。图书馆地下一层有24小时开放的休息室,里面有咖啡机、微波炉和自动售货机。
他拿出自己的杯子:一个在斯坦福二手店买的马克杯,蓝色,上面有褪色的斯坦福标志,1美元。他喜欢这个杯子,因为它有使用痕迹,不完美,像他自己。
咖啡是免费的,图书馆提供的,质量一般,但足够提神。他按下按钮,热水冲进速溶咖啡粉,形成深色液体。
他端着咖啡回到座位。热气在图书馆温暖的空气中不那么明显。
他拿起笔,在杯子的纸质杯套上画了一个图案:一颗星星,稍微有点歪,因为杯子是圆的,不好画。
高中时,陆星衍发现他有画星星的习惯后,曾经问:“你为什么总是画五角星?不画其他形状?”
“因为五角星最简单,”沈清辞说,“五笔就能画完。”
“但六芒星也简单。”
“六芒星要六笔,多一笔。”
陆星衍当时笑了——那种很少见的、真正被逗乐的笑:“你连这个都要省?”