本文完稿于2025年2月初
一直想写写这几年学习中的一些感想,最近总算是有时间了 。从上大学后第一缕缥缈思绪的出现说起吧。大一的上学期,由于本校的数电老师讲的太烂,我在 b 站上找了一个清华王红老师讲的数字电路课程。直到今天,我仍然认为这是我看过的最有逻辑最能抓住主线的教学。这门课的主要逻辑在于,计算机的 CPU 是一个很复杂的系统,而理解这样的复杂系统就需要掌握“抽象”(abstraction)的能力。正如我之后基本通读过的另一本非常棒的教材所说,
The critical technique for managing complexity is abstraction: hiding details when they are not important.
—— Chapter 1, Digital Design and Computer Architecture
计算机科学中的抽象
这门课讲的是,计算机体系结构可以被分为许多抽象的层级——最低的是物理层,就是电子啥的那一套。而这些微观层面粒子的物理性质可以让我们得到晶体管,进而得到模拟与数字电路,诸如各种 AND, OR, NOT 门。这又已经到了另一个抽象的等级。我们在纸上画出这些逻辑门的时候,在思考的总是“这些逻辑门应该如何被使用?”而不是“这些逻辑门为啥 work?”,因为在上层的抽象层级中,下层的抽象层级中的一个复杂的东西会被看做一个整体给“封装(encapsulate)”起来,然后不再管其中的任何细节。以此类推,逻辑门会被用来构成能够表达更加抽象的逻辑概念的组合与时序电路 (如 D flip-flop),然后再用这些电路去实现计算单元使之能进行加减乘除,等等。最终,在最高的逻辑层级,我们获得了一个能够执行许多抽象指令的 CPU。王红老师的课好就好在,她在一开始就道明这个思维的方式,并在之后讲述每一个抽象等级的时候都反复提及,“我们每当得到一个复杂的东西的时候,想到的第一件事就是把他们封装起来,把他们作为一个整体来使用。”
十分巧合的是,那学期我还上了另一节课,面向过程的编程(Python)。很幸运地,上这门课的 Marc Feeley 作为一位研究方向为 programming language 的老教授,并没有像外面烂大街的 Python 补习班那样框框介绍 python 的各种 features,而是重点介绍了其中函数,以及函数式编程的概念。这些概念的核心是 Function decomposition, 即我们想要实现一个功能,应该把这个需求一点点地拆解。这其实就对应这数字电路中 abstraction level 的划分。具体地,套用 abstraction 的术语来说,我们想要的 function 是很高的 abstraction level,而我们需要做的是设计一些更低等级 abstraction 的 function 来实现这个高等级的 function。同样的道理,这些低等级 abstraction 的 function 则可以通过更低等级的 function 实现。最终我们将来到最低等级的 abstraction,在这一层我们仅仅需要实现一些很简单的 function,比如基本的加减乘除,可以把他们叫做 auxiliary function。(当然,每一级的 function 都可以作为更高等级的 function 的 auxiliary function. 每一层的函数都是一个封装好的工具箱,就好像每一层级中的逻辑电路一样。对于上一层的使用来说,我只关心下一层实现功能的 input output 是啥,其内部的实现都不重要。调包的程序员读文档时,大部分也只会关注输入输出,同时相信这个包没有 bug。
抽象/抓住主要矛盾 V.S. 认知/学习
我们看见,不论在数电或者通用的编程中,都强调了抽象这一概念。更进一步,这个道理并非仅限于计算机科学,亦非仅限于理科。我们应将其上升到指导我们学习各个学科的一种思想,即“科学的科学”,一种哲学思想。在哲学这一层面上,这一伟大的思想并非陌生——在唯物辩证法中,有类似的提法,就是,“抓住主要矛盾”。
…… 研究任何过程,如果是存在着两个以上矛盾的复杂过程的话,就要用全力找出它的主要矛盾。捉住了这个主要矛盾,一切问题就迎刃而解了。 —— 毛泽东 《矛盾论》
老毛的《实践论》进一步指出,人们是通过对于各种事物内部与之间的矛盾的分析,以及对于主要矛盾的把握,来逐渐理解和认识这个世界的。在高中哲学课上我第一次懵懵懂懂地接触了这些思想。当年的我不曾理解其中的奥妙,靠着死记硬背的方式将这句话镌刻在了脑海里。真理永不褪色。在这之后的生活中,我所看见,所经历和所感受的世界就好像在挥舞着教鞭的小学老师,不停地敲打着这几个字,让我开始思考并且明白它们在现实生活中的意义。
认识和理解世界的第一步是抽象出一些概念。可以想象,当一个婴儿呱呱坠地后生命最初的几年,ta 全身的感官每天都在接受浩如烟海般的信息轰炸。这样饱和的信息对于没有任何信息过滤能力,或者说,认知能力的婴儿来说,无疑是一种过载。 因为大脑虽然很牛逼,但也不可能将生活中的每一个细节都原原本本地记录下来,细节永远是无穷无尽的, 就好像一张照片的像素可以任意高。如果婴儿不能够对于这些细节作出总结和化简后存入脑海, 则必然陷入“听数学老师讲天书”这样摸不着头脑的局面。我记得有一些猜想指出,人类在生命的最初没有记忆的原因正是由于语言系统的缺失。这是因为,语言本身就是对现实的一种抽象。而抽象的能力正代表着认知能力。这种能力需要我们人类在小时候花很大的力气去学会。比如说,随着成长儿童渐渐才能了解到,“椅子”这个概念包含了所有带靠背的用来坐的东西,不论其形状,材质或是颜色。而“凳子”则指那些没有靠背的椅子。掌握了这个概念以后,孩子们的记忆就不再是一系列庞杂的像素图像组成的动画,而是一个个概念串起来的简洁小故事,更加方便记忆。这就好比如果用像素图储存一张图片需要许多内存,而用矢量图则少很多。举另一个用概念记忆大量细节的例子。这两年通过记梦,我发觉晚上做的梦虽然在白天容易被遗忘,但其实记录其中的几个关键词后,即使白天完全忘记,看见这几个关键词之后也可以完全回想起来梦境丰富的剧情。以上的例子正是通过总结事物共性,抓住主要矛盾这一方法来减少大脑需要处理的信息量进而增强自己认知能力的。这些关于抽象 vs 人类认知的思考也有助于从另一个角度理解 deep learning 中的一些方法。比如在深度学习中,注意力机制可以帮助模型学习“抓住主要矛盾” (credit to ke chen),即更关注输入中与任务相关的部分,而忽略不重要的部分。CNN 中,越深层的 convolutional layer 的 feature map 中呈现出模型专注图像的部分也从细节导向变为抽象的语义概念导向。 Representation learning 的目的也是构建对复杂输入的简洁向量表示。(这个存疑,因为 embedding 似乎并不比原始的输入更简洁。这里需要再想想。从这个角度说,现有的 embedding models 应该还有提升的空间。)Classification 的任务之所以可以被用来训练特征提取器,就是因为 classification 要求模型具有抓住主要矛盾的能力。
还可以再抽象一点
大一的下学期我学习了“面向对象编程”这一课程,了解了“class”这一概念。当时我意识到,其实“class”们就对应着生活中的各种概念。比如我可以建立一个叫做“bird”的 class。而 attributes 代表了我们关心的主要矛盾 (们)。例如,如果在我们应用“bird” class 的场景中需要知道鸟儿羽毛的颜色,就可以定义叫做“feather_color”的 attribute。面向对象编程的好处就在于通过定义不同的 attributes 和 methods 来模拟在不同情境中主要矛盾的变化。更进一步,由于 superclass,interface, 对泛型的支持等机制的存在,面向对象编程还提供了不同的抽象层级,因而成为“建模”这个世界的有力工具。
和面向对象编程的理念类似,“建模”世界的第二步就是,在生命最初认识到的这些基础概念上抽象出更高抽象层级的概念并最终建立起一个抽象层级(hierarchy)。这需要对这些概念中的矛盾的进一步分析。比如一个牙牙学语的小孩看见了路边的“黑长直漂亮学姐”后妈妈告诉 ta 这是“喜鹊”。第二天小盆友又被松枝上嘎嘎叫的乌鸦吸引。第三天认识了乌鸫,第四天认识了灰喜鹊,第五天认识了麻雀,等等。这时候,妈妈会告诉 ta,这些都是“小鸟”。也许以后的对话中,妈妈就会告诉孩子,“看,那边的树枝上有一只小鸟!”这就完成了一次从具体种类鸟的概念到广泛的鸟类的概念的抽象。最终,孩子在看纪录片的时候看见没见过的信天翁的时候会不由自主地感叹道 “哇,这只鸟好大呀!”这时,ta 就已经能够利用抽象出的“鸟”这一概念去分析未知的事物了。
进一步的抽象可以通过学习来进行。比如初中生物告诉我们生物分类有“界门纲目科属种”,而鸟儿们只是这逻辑大树的一片片树叶罢了。我有一个对生物分类非常痴迷的朋友(olgd)在了解了这一体系之后对于身边的每一个植物都能准确地说出其在体系中的位置。我现在非常理解他的痴迷,这是对高级认知过程的享受。另一个例子是历史学。学会了语言之后我们就可以将中国的历史写成一本书。但是在此之上是否还能进一步抽象出一些规律呢?这大概就是历史学的使命吧。比如从中国历代王朝的兴衰可以看出他们大部分都毁灭于土地兼并导致的中央财政收入不足地方势力尾大不掉/士大夫阶级卖国求荣等等。物理学定律也是一个例子(感谢 mtl 第一美男子提供这个例子)。牛顿能够从事物的运动规律中抽象出 F=ma,说明力学也是一种抽象。数学更是抽象中的抽象。这个大家想必都承认。大学的时候我校线性代数老师讲的也不好,我在 b 站上学习了邱维声老师的高等代数。令我印象深刻的是邱老师“解剖麻雀”这一理解线性代数的方法。其基本的思维框架是,从欧氏空间或者生活中的简单例子出发引导同学们自主地抽象出更普遍的线性空间中的各种概念。正所谓“麻雀虽小,五脏俱全”,通过对一个 class 的 instance 的分析,也能让我们明白这个 class 的特点。Deep learning 中,CV 比 NLP 更难 scaling 是因为 CV 模型需要从复杂的图像世界开始先抽象出其中的语义信息,然后若需要模型理解更复杂的语义概念的话,应该需要更多的数据去训练模型在(从图像上抽出的语义概念)上进一步抽象的能力。这是一个两阶段的学习,显然更难训练。
从另一方面讲,认识和理解这个世界其实还是需要除了抽象之外的思维活动,比如逻辑推理,联想等等。这些思维活动并不能完全被归纳在“抽象”的体系内。联想也许可以。比如两个事物之间有联系,很可能是因为他们在所属的 abstraction hierarchy 中的距离比较近。比如喜鹊和灰喜鹊的联系在于他们都是鸟类。(界门纲目科属种我就懒得查了哈)逻辑推理似乎是完全不同于抽象的能力,但我们应当承认学会这种能力首先要会抽象出一些概念,这样才能在概念之间寻找更多的关系。更进一步,越抽象的概念之间的推理往往会越简洁(存疑?)。
无法忽略的次要矛盾
以上是这几年来我对“抓住主要矛盾”的感悟。如果读者还记得哲学课本上的内容的话,一定不会忘记与“抓住主要矛盾”共生的“不能忽略次要矛盾”。最典型的例子其实是在科研中的建模问题。我们希望把实际问题抽象成一个数学模型,然后用这个数学模型相关的结论解决问题。但在抽象的过程中,我们往往会做一些“假设”。比如 NLP 中最经典的 bag of words 的 language model 是假设 $p (w_{1},w_{2},w_{3}) = p(w_{1})p(w_{2})p(w_{3})$ , 即每个词的出现是独立的。这样的假设可以简化模型,却不能反映真实情况。也就是说,忽略了不应该忽略的次要矛盾。事实上,在语言建模时还应考虑到每个词的左右的 context 以及整个序列的顺序。考虑了这些问题后得出的 masked language modeling 或者 auto-regressive language modeling 更加合理。事实上,用 auto-regressive language 训练出来的 GPT 也并未完美,因为语言本身就是通过忽略很多现实世界中的细节(次要矛盾)抽象而来,所以也许多模态的语言模型有 visual grounding 的话能够更好地补全次要矛盾,建模这个世界。我想,在科研中时刻问自己“这个方法做了什么假设?如果放宽假设的话方法是否可以被改进?”等问题是有助于思考的。其他的例子包括,我们在用泰勒展开式时会根据精度的需要考虑几阶展开,此时被忽略的余项就是次要矛盾。当精度的需求提高时,这些余项则不能被忽略。程序员在调用一个函数的时候如果遇到了函数内部的 bug,则不能只看 documentation 提供的 input,output 信息,需要深入函数内部去 debug。这时函数的 implementation 就成为了不可忽视的次要矛盾了。
另一个关于无法忽略的次要矛盾的例子是各级政府之间的关系。上级政府给下级政府一些任务,下级政府开会讨论并具体执行,然后把结果汇报上去。上级政府也无法事事包办,所以他们会把精力放在决策上,这种情况下也就只能假设下级的汇报是可信的,而执行的细节此刻就变成了上级政府眼中的次要矛盾。(突然想到“这些事情,你们去办,我的事多,我要把精力放在军事上 XP”)如果下级政府欺瞒上级政府,上级政府很难发现。上级政府可以设立监察机构来确保次要矛盾也如上级政府想象中地完美执行,然而是检查机构本身也是个 hierarchy, 也会存在欺上瞒下这种情况。就有点像屎山代码中的函数很难去信任。由此可见,抽象真的是一门艺术,因为同时兼顾主要矛盾与次要矛盾往往由于资源的缺乏而无法实现,正所谓鱼与熊掌不可得兼。在这种情况下,发挥我们的主观能动性去具体问题具体分析,正确、勇敢地分析出事物在不同条件下的的主要和次要矛盾是一种宝贵的能力,或者,意志品质。
生活中的抽象正义
生活本身也是一个复杂的系统。在科技前沿日新月异的信息时代,每天仿佛有看不完的新闻,数不清的机会,做不完的事情。人总不能方方面面都完美,既要又要的话往往会左支右绌。也许我们需要时时刻刻聆听自己的心声:“当下对我最重要的是什么?”在认真地分析之后,如果能够得到一个内心笃定的答案,那么就放手去做吧!
谨以此文献给我即将开始的博士生活
