Categories

同义反复

忘记了之前在哪里看到说数学其实就是同义反复而已。从某种程度上来说,这样的言论也不能说完全是乱说,比如一系列的等价的推导,其实可以说就是在说同一件事情。但是“同义反复”多少有些贬义的意思,具体来说应该是指“不必要的反复”吧,但是数学应该不是这样吧。实际上,来考虑一下什么样的“反复”是“不必要的反复”就可以了,我觉得,那些只要是“不太明显” (non-obvious) 的关系,把这样的关系建立起来,应该也都是有其意义的,而尤其重要的是其中那些“深刻”的关联。

不过“深刻”这样的词是不是太抽象了呢?实际上,最近一年一来,接触了些数学专业的人,在同他们讨论问题的时候——好吧,其实大部分时候是我在听他们讲问题的时候——“深刻”这个词便时常在我脑子里出现。有时候听到他们说一些东西,会觉得很震惊,惊讶“原来如此”,惊讶自己从前的理解是如此的“肤浅”和不得要领。然而我一直想要来描述“深刻”这个词,却一直没有想法。也许应该举一个例子,不过有许多例子一时也想不起来了,也许有很多比较合适的经典的例子,比如 5 次以上方程不可用根式解之于 Galois Theory 的联系,我却又没法讲出来。

实际上大致就是那种感觉,不仅仅存在于数学,也存在于任何学科任何领域。从某一方面来说,世间的万事万物,作为一个个的独立的存在的话,并不是什么重要的存在,反而是它们之间的相互关联更加重要一些。所以,如果看到一个现象,得到的只是一些很明显的联系的话(也就是所谓的 obvious 的东西),也就是所谓肤浅了,这样用处大概并不大;但是如果是能抓住更深层次的东西(也就是 non-obvious 的东西),往往就能把问题看得更透彻——这样的带来的优势是可以比较形象地比喻的。比如说,各大洲上的生物有一些具有非常高的相似性,如果能顺着这个线索最终追查出原来曾经几块大陆是连在一起的,那么不仅为什么相差十万八千里的生物具有很高的相似性的问题变得豁然开朗,而且可以由此得到更多的结论来。

深刻,也就是抓住本质的东西,就好比照妖镜照出妖怪的本来面目。唐僧看不见妖怪的原形,所以妖怪用一些“烟雾弹”轻易就让唐僧上当受骗了;但是孙悟空有火眼金睛,却能看透妖怪的本质。这似乎让看透本质这样的能力越来越虚幻起来。但是神话毕竟只是神话,现实中没有人能有火眼金睛,但是看问题看得深刻——或者说,看到本质的东西,这样的能力却并不是遥不可及的。

比如关于大一的时候学的《数学分析》,当时学的细节我已经忘记了,但是却还记得似乎是有 7 个公理(什么闭区间套定理、上确界存在性之类的),它们之间可以互相推导出来,当时被这些东西折腾得死去活来,已经完全陷入了各个公理直接互相如何证明的细节之中,虽然数学分析学完了,即便把证明技巧都记住了,考试也考过了,却仍然在迷雾中。直到后面接触到点集拓扑(以及其他一些相关的东西)的时候,便明白那句话的道理了“其实讲了半天也就是在说实数里的紧性和实数的完备性”。泛函分析也是类似的吧,之前听到旁边有人在讨论泛函分析的时候,一个人说,其实就是一个拓扑向量空间。我当时没有明白这句话的含义,但是后来接触到一点拓扑群的东西之后,突然就想起那句话来,也许这也是背后的东西吧!

当然事物的本质并不代表它的全部,余下的细节仍然重要,只是抓住了本质的东西,便不太容易迷失在细节中了。其实对于程序员来说,应该更加熟悉的一个例子是阅读源代码吧。读别人写的代码,对于程序员来说,永远是一个大难题,不论是欣赏优雅的代码,还是皱着眉头握着拳读乱得一团糟的代码,总是会遇到的一个问题就是很快就会陷入无止尽的细节当中迷失掉。哪些函数应该暂时跳过,哪些细节是必须要在当前弄清楚,而哪些又是不重要的。把握好这些东西,其实也就是在尝试抓住这个程序的本质。

实际上,我们实验室里因为同时有计算机系和数学系的人,所以有时候我就会饶有兴致地去观察。有一次,一位数学系的小朋友在看一段代码的时候碰到一个问题,跑来问我,说这个 stack 的 pop() 函数为什么返回了一个什么东西呢,可是我在书上看到 pop 是没有返回值的啊。我想也许他看的书上说的是 C++ 标准库里的 stack 的文档吧,而这段代码明显是作者自己另外搞了一个简易的 stack 实现。于是有人要说了,怎么能盲目地跟着书上怎么说就是怎么样呢?再把视线往前移一些距离看到那个关于 stack::pop() 的定义,不就知道实际上是怎么一回事了吗?然而我想这里的问题正是那个——因为没有能透过长篇大论的代码看到背后的主线,而在无关紧要的角落里的细节里迷失了的缘故吧。

这么针对性地举这个例子其实是想说明这个问题:“深刻”地看问题的能力其实并没有什么神秘的,前面说了,数学系的人很多时候说出一些让我觉得恍然大悟的话来,一直让我深感佩服;但是在另一些方面他们却也有看不透的时候。也就是说其实这样的能力是和具体领域或者说具体问题相关的,因为说到底,“看透本质的火眼金睛”原来并不是什么神秘之物,孙悟空在炼丹炉里烧上七七四十九天而得到看透妖怪的本领,“同样”的事情,我们也可以做,那就是经验、或者说是知识的积累。

所以呀,还是要踏踏实实地学习…… -.-bb (我变老头子了),而且为什么基础知识变是那么重要也显而易见了。如果把由基础的东西衍生出来的种种细节看成一棵树的话,通常要找到两个相隔很远的枝桠的本质关联,实际上就是回溯到它们共同的树干那里,也就是基础知识那里了。也许正是这样所以 C 语言的学习(至少对于计算机专业来说)是相当有必要的吧。突然冒出这样的想法的原因大概还在于我暑假把萝卜和石老师的那本《程序员的自我修养——链接、装载与库》给看了一遍的缘故——虽然里面还有不少东西未能完全吃透以至于看完之后又忘记了不少内容(-.-bb 长久不接触工程上的东西也是一大原因吧),但是还是觉得受益匪浅。实际上,当时总是觉得,为什么计算机系不好好教编程(只有一门 C 语言课,还是几万年前的 TC ,各种不断涌现的新技术学校里一点都不会讲),却去教那些看起来一点用也没有的什么《操作系统》——到了这个年代,难道真有人想要去从头写一个全新的操作系统,能打败 Windows 、Linux、 BSD ?《编译原理》——除了那极个别的人之外,谁会去做编译器的开发,谁又会关心文法解析器是如何工作的?还有《计算机组成》、《计算机网络》甚至还有讲史前时代故事的《汇编与接口》这样的课。可是到学到《体系结构》的时候,我似乎有些慢慢开窍了,好像一些没啥关系的东西慢慢互相连接起来。

也许小小地醒悟的那一刻是在学到 cpu 分支预测的时候吧,因为之前在看线程安全性相关的东西的时候,有看到说由于指令被换序而导致的问题,当时一直不能理解:觉得不同的线程的指令执行先后顺序交替起来还是可以接受的,同一个线程的指令怎么会前后交换了?怎么都不可能啊!或者说,完全没有必要啊!这也就是理所当然地想当然的结论了,实际上也就是所谓“肤浅”的观察,而等到我看到这个问题的“本质”——也就是 cpu 究竟是怎么来执行指令的时候,乱序的问题一下子就变得理所当然了。

结果嘛,骂了那么多年的课程设置,到最后来其实发现还是自己太 naive 了。“要是当时知道如此,就该学得更认真一些了”这样的话,还是就不要说了吧 😀 。总的来说,完全一头扎紧传统的课程里不管工业界的技术发展,似乎并不是一件好事,但是另一方面学校的课程也并不是没用之物。事实上,要读懂《程序员的自我修养》这样的书,不对操作系统、编译原理、体系结构等都有一定的了解的话,大概也是很吃力的。

最后再举一个例子吧,实际上是老问题了。关于 mmseg-cpp 的问题。一个分词的东西,最开始用 Ruby 写的,太慢了,后来用 C++ 写了,提供了 Ruby 和 Python 的接口,包装成 rmmseg-cpp 和 pymmseg-cpp 。似乎还是挺受欢迎的,因为比较小巧速度也比较快而且刚好就只干一件事情,基本上拿来就可以用了。不过我大概快有好多年没有维护过了,因为最近几年一直在搞“科研”嘛。实际上前一阵子实验室里也有尝试过拿这个包来用,当时用的是 pymmseg-cpp ,出现了一些问题,由于我当时不太有时间,所以也没有尝试去修正——不过心里还是 keep in mind 了,因为 bug 很好重现,而且我稍微尝试了一下,同样的步骤在 32 位机器上是没有问题的,在 64 位机器上就挂掉了——碰到特定的字符串,输出的结果就完全乱掉了。由于最初开发的时候是在 32 位机器上开发的,虽然我已经比较小心了,但是毕竟当时没有 64 位机器的经验,也许在某些优化步骤的时候使用了不太可移植的手段的缘故?所以就暂时放弃了。

近一阵子不是又被实验室抓来做一些项目开发相关的东西吗?所以 C++ 什么的又稍微捡起来了一点,刚好也收到关于 rmmseg-cpp 的 bug 报告(稍微有点诧异,如果是我自己,除非是迫不得已,否则对于超过一年没有新 commit 的开源项目,我一般是不太会去用的 ^_^bb )。于是就索性去检查一下代码吧。因为我认定是由于 32 位/64 位的差异引起的问题,所以最直接的办法就是把代码全部看一遍了,幸好代码其实很短的。

关于 pymmseg-cpp 里的 bug ,我是如何发现的,已经给忘记了…… @_@bb ,总而言之似乎把代码全部看了一遍,中间顺手改了一些地方,后来又被我给改回来了——因为发现最开始其实是对的,反而被我改错了。结果问题解决了,其实还是 GC 的问题:Python 里调用 C++ 是通过 ctypes 来实现的,中间把一个 Python 里的 text buffer 传到 C++ 里之后,那块 buffer 后来被 GC 给干掉了。这样一来,似乎 32 位机器上也有可能出现问题吧(汗……),不过确实不是 32 位 64 位的问题了。解决办法是把那个 text buffer 的引用保留下来。其实我想说的是,和今天讲的话题相符合的是:看到 32 位机器和 64 位机器上的行为不同,就认定是字长导致的问题,这就是比较肤浅的关联,而最后找到是 GC 的问题,这就是相对深刻一些的关联。不过不幸的是我把中间过程给忘记掉了……所以这个例子不能细讲了。

不过幸好 rmmseg-cpp 也出现了 bug (-.-bb),而且 rmmseg-cpp 由于 Ruby 没有 ctypes 这样的库,接口是通过 Ruby native interface 来实现的。写 rmmseg-cpp 的那个年代还是 Ruby 1.8 当道,现在系统默认的 Ruby 版本都是 1.9 了,其实能直接编译过(唔,其实似乎还是因为接收了别人的 patch 才能编译过的)我都已经比较诧异了。

总之后重现 bug 的状况是:有时候会出现 segment fault ,还能得到一堆 backtrace (自从用了 Python 这个奇特的语言之后,我就再也没有分清楚过各种语言打印出来的 stack trace 的顺序到底是从深到浅还是从浅到深的了……)。最后检查出来是在把一个 token 的 text 这个属性打印出来的时候挂掉了。对 Ruby 有点了解的话,会知道这里实际上是去调用了 text 的 to_str 函数,为什么调用这么一个看起来很无害的函数会挂掉呢?于是便去检查 text 这个变量到底是怎么回事,对 Ruby 语言的实现方式有一点了解的应该知道,Ruby 实现 Duck Typing ,Ruby 级别的对象,在 C 里其实都是一个指针那么长的东西,对象的类型被编码在这个值里,如果是像小整数之类的类型,则将该值直接编码在剩下的位里,其他的通用对象类型,则存放的是一个指向对象实际内存块的指针。总而言之,可以通过那个值得到该对象的类型,ruby.h 里有 TYPE 这个宏就是用来做这个事情。

尝试之后发现 text 的类型是 0 ,也就是 NONE ,或者是不对应任何类型。这样的对象根本不是合法的,明明是合法地构造出来的对象,怎么又变成 0 了呢?这八成是被垃圾回收器 (GC) 给回收了。到这里的时候,其实故事该结束了,因为我猛然在自己的代码里看到一句注释,里面还有一个 URL ,链接到自己以前的一篇 blog 文章,讲的居然就是这个事情,而且当时给了解决方案,但是似乎当时清扫的时候遗漏下了几个地方,于是……。所以详细情况还是看那篇 blog 吧,简单的来说,就是,Ruby 的 GC 把 text 这个成员变量给回收了。但是 Ruby 的 C API 其实是提供了函数来让对象在进行 GC 的时候把自己的成员变量 mark 住防止被回收的,我也使用了这样的机制,但是这里的问题在于:text 对象被回收的时候它所属于的那个 token 对象还没有构造好,也就是说 C 代码执行到一半的时候 Ruby GC 运行起来了。解决办法是弄一个临时的局部变量来存 text ,并且还要设置为 volatile 。要明白是怎么回事,需要对 Ruby GC 的 mark-and-sweep 算法有一个大致的了解,也要知道多线程调度的时候会出现的执行顺序问题,还要知道程序运行时候的存储机制(堆和栈、寄存器等)以及函数调用的 calling convention ,还需要一点编译器编译的时候如何对代码进行优化的知识。

嗯,说了这么多,其实就是想说的是,要能把问题看得更深一些,其实并不一定是一种神奇的难以达到的能力,但是大部分时候却也没有什么捷径可言。就连美猴王也是费了那么大的劲才得到神眼,那我们呢?如果受不住七七四十九天的三味真火猛烧的话,那还是走另一条路:先关掉浏览器,去好好学习一会儿吧! 🙂 到头来还是越写越乱啊,怎么变成了励志贴似的? ^_^bb

13 comments to 同义反复

  • 我觉得是这样子的: 比如说, 如果说两个正确的数学命题A,B在ZFC下总是可以认为是等价的, 那么数学上真正有意思的是怎么样具体的将A,B联系起来, 它们之间存在什么样的关系, 可以做哪些不同的处理, 可以得到什么变形, 可以做哪些有趣的不同的尝试, 它们之间的关系才是引人兴趣的地方, 而不是它们的”正确与否”. 比如说我们知道晚上总是能见到月亮, 而海水会固定的涨潮退潮, 假如我们认为”这些都是绝对正确的”, 当然我们可以说, 太阳底下没有新鲜事, 所有这些就是发生了, 反正我信了, 如次之类, 似乎这两个已知成立的命题就没有更多意思了. 但是如果问那么怎么样建立起两者之间的联系呢, 牛顿经过数年工作给了我们引力理论来解释, 并且后来有了对这些工作的改进, 并且把它们在生活中应用起来, 如果要问所有这些努力最终会怎么样呢, 所有这些研究又有什么意义呢? 只能说The final answer is 42..

    • 嗯,确实是这样的,老师也会强调,集合本身并不是最重要的,最重要的是集合之间的映射。好像范畴里也是这个模式,objects 本身并不如 objects 之间的 morphism 来得重要。反正就是那样一种感觉,要说清楚具体是什么意思似乎也不是一件容易的事情。

  • 欧阳锋

    我在学习的过程中,发现很多时候,都陷入分类加穷举的无尽消耗中,所谓的灵感却始终没有!

    你说的东西,我觉得就像灵感。

  • yinhai666888

    我被你打败了!太佩服了,我最近也在看支持向量机,感觉和你的差距太大了。我是玉泉的,呵呵,很高兴玉泉有你这样的牛人,赞一个!

  • winsty

    当年Pei Jian来上课,讲的什么基本已经不记得了,印象最深的就是他说的一句话,如果我的学生seminar的时候不能把一篇paper概括成一句话,那么肯定就是没理解透这个paper

  • 哈哈,多谢你的帮忙,我就是那个麻烦。ruby上面就你这个库好用一点,如果不能用,那就太遗憾了,希望你能继续维护这个项目。

    我好像还是你的学长,呵呵,我是08年ZJU毕业的。

  • naturehuan

    Great to read it! 数学系学生张桓 from BUAA(目前大四)表示数学远远不在于其所作的同义反复工作,也不仅仅只是zchen所说的具体联系的方法。我个人的体会是数学最优美的地方在于凭借数学逻辑所演绎出的完美而宏大的架构,现实世界只不过是其架构中的一个存在而已。纯粹数学并不需要考虑具体的对象,而是寻找到无穷的可能来包容所有具体的对象。源于现实,又远高于现实,使得最纯粹的数学家陷入理想的完美中无法自拔。自从有了极限,数学便脱离已知世界而独立存在。很喜欢你的结论,追求事物的本质。可以的话希望能交个朋友,我对计算机还真一窍不通,但特别关注生命的议题。

    • 你好!很高兴认识你!数学的架构确实是非常美妙的一个方面。生物生命什么的我不了解,但是仅从架构方面来说,有些时候数学和计算机(程序设计)还是比较类似的:架设一个体系。或者说,如果说生命是自然的杰作的话,数学和计算机倒是都是人类的创造呢。

  • 看到你寫到在程序裡留注釋和鏈接的時候,深有同感。
    很遺憾人的記憶有限……當時想清楚的一些細節問題過後就會忘掉。
    要把瑣碎的細節記錄清楚不容易,所以不得已只能留個 reference 當作以後備忘。

  • jfor

    佩服您对事物的洞察力。你有一些博客总能写出很深刻的感触。