More servicesWindows Live
HomeHotmailSpacesOneCare
 
MSN
Sign in
 
 
Spaces home  笑对人生,傲立寰宇PhotosProfileFriendsBlog Tools Explore the Spaces community

Blog

July 19

Boston暑假的生活

这篇blog和学术没有关系。前面连篇累牍的东西,我想很多朋友也看得挺累的了,这次就先歇一歇吧,讲点学术以外的生活。

严格来说,我们现在正在放暑假,不过,说实话,除了不上课外,和平时也没什么区别。入夏以来,这里的气温一天高比一天——今天的最高温度已经突破95华氏度了。住的地方没有空调,这对于我这个长期生活在南方,对空调有严重依赖的人来说,确实是一个很有挑战性的体验。

因为不用上课了,生活也开始丰富了一些。

暂停了一年多(hmm... 不知道这还叫不叫“暂停”)的羽毛球运动恢复起来了。看来,美国人确实对badminton不太感冒,MIT的体育馆虽然只有四五个场子,我们平常去打的时候,经常都有空场,在里面打球的很多是中国同学(当然也包括港澳台同胞了,呵呵)。也有一些老外在里面打球,水平还是很不错的。

我的水平也和在HK的时候差不多——难得的是,大家都在附近的区间,最近几次连续很多场双打,都是(16:14)这样的比分(2006年以前的比赛规则)。打得多了,有点瘾子,心血来潮之下进了一把新拍子。其实,以咱现在的能力,用好拍子也不见得能发挥多少优越性,只是小小的心理满足罢了。

另外一项活动,就是风帆了——这就是学校在河边的好处。我原来正好却一个partner去学sailing,xiaoqian转来MIT之后,正好和他一起去学习这个玩意。上过两次课后,我们现在已经可以在查尔斯河驾驶小帆船了。有风的时候,乘帆游河确实是一种惬意的生活——不过,“乘风破浪会有时,直挂云帆济沧海”的豪情,在这小河上就万难体验了。

因为上船前物品都要寄存,所以,现在还没有我在船上的照片。就先贴几张别人拍的charles river上MIT的风帆遨游的情景吧。

mit_sailing_1

mit_sailing_2

mit_sailing_3

July 13

介绍几本数学书

前面几篇谈了一些对数学的粗浅看法。其实,如果对某门数学有兴趣,最好的方法就是走进那个世界去学习和体验。

这里说说几本我看过后觉得不错的数学教科书。

1. 线性代数 (Linear Algebra):

我想国内的大学生都会学过这门课程,但是,未必每一位老师都能贯彻它的精要。这门学科对于Learning是必备的基础,对它的透彻掌握是必不可少的。我在科大一年级的时候就学习了这门课,后来到了香港后,又重新把线性代数读了一遍,所读的是

Introduction to Linear Algebra (3rd Ed.)  by Gilbert Strang.

这本书是MIT的线性代数课使用的教材,也是被很多其它大学选用的经典教材。它的难度适中,讲解清晰,重要的是对许多核心的概念讨论得比较透彻。我个人觉得,学习线性代数,最重要的不是去熟练矩阵运算和解方程的方法——这些在实际工作中MATLAB可以代劳,关键的是要深入理解几个基础而又重要的概念:子空间(Subspace),正交(Orthogonality),特征值和特征向量(Eigenvalues and eigenvectors),和线性变换(Linear transform)。从我的角度看来,一本线代教科书的质量,就在于它能否给这些根本概念以足够的重视,能否把它们的联系讲清楚。Strang的这本书在这方面是做得很好的。

而且,这本书有个得天独厚的优势。书的作者长期在MIT讲授线性代数课(18.06),课程的video在MIT的Open courseware网站上有提供。有时间的朋友可以一边看着名师授课的录像,一边对照课本学习或者复习。

http://ocw.mit.edu/OcwWeb/Mathematics/18-06Spring-2005/CourseHome/index.htm

2. 概率和统计 (Probability and Statistics):

概率论和统计的入门教科书很多,我目前也没有特别的推荐。我在这里想介绍的是一本关于多元统计的基础教科书:

Applied Multivariate Statistical Analysis (5th Ed.)  by Richard A. Johnson and Dean W. Wichern

这本书是我在刚接触向量统计的时候用于学习的,我在香港时做研究的基础就是从此打下了。实验室的一些同学也借用这本书学习向量统计。这本书没有特别追求数学上的深度,而是以通俗易懂的方式讲述主要的基本概念,读起来很舒服,内容也很实用。对于Linear regression, factor analysis, principal component analysis (PCA), and canonical component analysis (CCA)这些Learning中的基本方法也展开了初步的论述。

之后就可以进一步深入学习贝叶斯统计和Graphical models。一本理想的书是

Introduction to Graphical Models (draft version).  by M. Jordan and C. Bishop.

我不知道这本书是不是已经出版了(不要和Learning in Graphical Models混淆,那是个论文集,不适合初学)。这本书从基本的贝叶斯统计模型出发一直深入到复杂的统计网络的估计和推断,深入浅出,statistical learning的许多重要方面都在此书有清楚论述和详细讲解。MIT内部可以access,至于外面,好像也是有电子版的。

3. 分析 (Analysis):

我想大家基本都在大学就学过微积分或者数学分析,深度和广度则随各个学校而异了。这个领域是很多学科的基础,值得推荐的教科书莫过于

Principles of Mathematical Analysis, by Walter Rudin

有点老,但是绝对经典,深入透彻。缺点就是比较艰深——这是Rudin的书的一贯风格,适合于有一定基础后回头去看。

在分析这个方向,接下来就是泛函分析(Functional Analysis)。

Introductory Functional Analysis with Applications, by Erwin Kreyszig.

适合作为泛函的基础教材,容易切入而不失全面。我特别喜欢它对于谱论和算子理论的特别关注,这对于做learning的研究是特别重要的。Rudin也有一本关于functional analysis的书,那本书在数学上可能更为深刻,但是不易于上手,所讲内容和learning的切合度不如此书。

在分析这个方向,还有一个重要的学科是测度理论(Measure theory),但是我看过的书里面目前还没有感觉有特别值得介绍的。

4. 拓扑 (Topology):

在我读过的基本拓扑书各有特色,但是综合而言,我最推崇:

Topology (2nd Ed.)  by James Munkres

这本书是Munkres教授长期执教MIT拓扑课的心血所凝。对于一般拓扑学(General topology)有全面介绍,而对于代数拓扑(Algebraic topology)也有适度的探讨。此书不需要特别的数学知识就可以开始学习,由浅入深,从最基本的集合论概念(很多书不屑讲这个)到Nagata-Smirnov Theorem和Tychonoff theorem等较深的定理(很多书避开了这个)都覆盖了。讲述方式思想性很强,对于很多定理,除了给出证明过程和引导你思考其背后的原理脉络,很多令人赞叹的亮点——我常读得忘却饥饿,不愿释手。很多习题很有水平。

5. 流形理论 (Manifold theory):

对于拓扑和分析有一定把握时,方可开始学习流形理论,否则所学只能流于浮浅。我所使用的书是

Introduction to Smooth Manifolds.  by John M. Lee

虽然书名有introduction这个单词,但是实际上此书涉入很深,除了讲授了基本的manifold, tangent space, bundle, sub-manifold等,还探讨了诸如纲理论(Category theory),德拉姆上同调(De Rham cohomology)和积分流形等一些比较高级的专题。对于李群和李代数也有相当多的讨论。行文通俗而又不失严谨,不过对某些记号方式需要熟悉一下。

虽然李群论是建基于平滑流形的概念之上,不过,也可能从矩阵出发直接学习李群和李代数——这种方法对于急需使用李群论解决问题的朋友可能更加实用。而且,对于一个问题从不同角度看待也利于加深理解。下面一本书就是这个方向的典范:

Lie Groups, Lie Algebras, and Representations: An Elementary Introduction.  by Brian C. Hall

此书从开始即从矩阵切入,从代数而非几何角度引入矩阵李群的概念。并通过定义运算的方式建立exponential mapping,并就此引入李代数。这种方式比起传统的通过“左不变向量场(Left-invariant vector field)“的方式定义李代数更容易为人所接受,也更容易揭示李代数的意义。最后,也有专门的论述把这种新的定义方式和传统方式联系起来。

————————————————————————————

无论是研究Vision, Learning还是其它别的学科,数学终究是根基所在。学好数学是做好研究的基石。学好数学的关键归根结底是自己的努力,但是选择一本好的书还是大有益处的。不同的人有不同的知识背景,思维习惯和研究方向,因此书的选择也因人而异,只求适合自己,不必强求一致。上面的书仅仅是从我个人角度的出发介绍的,我的阅读经历实在非常有限,很可能还有比它们更好的书(不妨也告知我一声,先说声谢谢了)。

 

July 09

Learning中的代数结构的建立

Learning是一个融会多种数学于一体的领域。说起与此有关的数学学科,我们可能会迅速联想到线性代数以及建立在向量空间基础上的统计模型——事实上,主流的论文中确实在很大程度上基于它们。

R^n (n-维实向量空间) 是我们在paper中见到最多的空间,它确实非常重要和实用,但是,仅仅依靠它来描述我们的世界并不足够。事实上,数学家们给我们提供了丰富得多的工具。

“空间”(space),这是一个很有意思的名词,几乎出现在所有的数学分支的基础定义之中。归纳起来,所谓空间就是指一个集合以及在上面定义的某种数学结构。关于这个数学结构的定义或者公理,就成为这个数学分支的基础,一切由此而展开。

还是从我们最熟悉的空间——R^n 说起吧。大家平常使用这个空间的时候,除了线性运算,其实还用到了别的数学结构,包括度量结构和内积结构。

  • 第一,它是一个拓扑空间(Topological space)。而且从拓扑学的角度看,具有非常优良的性质:Normal (implying Hausdorff and Regular), Locally Compact, Paracompact, with Countable basis, Simply connected (implying connected and path connected), Metrizable. 
  • 第二,它是一个度量空间(Metric space)。我们可以计算上面任意两点的距离。
  • 第三,它是一个有限维向量空间(Finite dimensional space)。因此,我们可以对里面的元素进行代数运算(加法和数乘),我们还可以赋予它一组有限的基,从而可以用有限维坐标表达每个元素。
  • 第四,基于度量结构和线性运算结构,可以建立起分析(Analysis)体系。我们可以对连续函数进行微分,积分,建立和求解微分方程,以及进行傅立叶变换和小波分析。
  • 第五,它是一个希尔伯特空间(也就是完备的内积空间)(Hilbert space, Complete inner product space)。它有一套很方便计算的内积(inner product)结构——这个空间的度量结构其实就是从其内积结构诱导出来。更重要的,它是完备的(Complete)——代表任何一个柯西序列(Cauchy sequence)都有极限——很多人有意无意中其实用到了这个特性,不过习惯性地认为是理所当然了。
  • 第六,它上面的线性映射构成的算子空间仍旧是有限维的——一个非常重要的好处就是,所有的线性映射都可以用矩阵唯一表示。特别的,因为它是有限维完备空间,它的泛函空间和它本身是同构的,也是R^n。因而,它们的谱结构,也就可以通过矩阵的特征值和特征向量获得。
  • 第七,它是一个测度空间——可以计算子集的大小(面积/体积)。正因为此,我们才可能在上面建立概率分布(distribution)——这是我们接触的绝大多数连续统计模型的基础。

我们可以看到,这是一个非常完美的空间,为我们的应用在数学上提供了一切的方便,在上面,我们可以理所当然地认为它具有我们希望的各种良好性质,而无须特别的证明;我们可以直接使用它的各种运算结构,而不需要从头建立;而且很多本来不一样的概念在这里变成等价的了,我们因此不再需要辨明它们的区别。

以此为界,Learning的主要工作分成两个大的范畴:

  1. 建立一种表达形式,让它处于上面讨论的R^n空间里面。
  2. 获得了有限维向量表达后,建立各种代数算法或者统计模型进行分析和处理。

这里只讨论第一个范畴。先看看,目前用得比较广泛的一些方法:

  1. 直接基于原始数据建立表达。我们关心的最终目标是一个个现实世界中的对象:一幅图片,一段语音,一篇文章,一条交易记录,等等。这些东西大部分本身没有附着一个数值向量的。为了构造一个向量表达,我们可以把传感器中记录的数值,或者别的什么方式收集的数值数据按照一定的顺序罗列出来,就形成一个向量了。如果有n个数字,就认为它们在R^n里面。

    不过,这在数学上有一点小问题,在大部分情况下,根据数据产生的物理原理,这些向量的值域并不能充满整个空间。比如图像的像素值一般是正值,而且在一个有界闭集之中。这带来的问题是,对它们进行线性运算很可能得到的结果会溢出正常的范围——在大部分paper中,可能只是采用某些heuristics的手段进行简单处理,或者根本不管,很少见到在数学上对此进行深入探讨的——不过如果能解决实际问题,这也是无可厚非的,毕竟不是所有的工作都需要像纯数学那样追求严谨。

  2. 量化(quantization)。这是在处理连续信号时被广泛采用的方式。只是习以为常,一般不提名字而已。比如一个空间信号(Vision中的image)或者时间信号,它们的domain中的值是不可数无限大的(uncountably infinite),不要说表示为有限维向量,即使表达为无限序列也是不可能的。在这种情况下,一般在有限域内,按照一定顺序每隔一定距离取一个点来代表其周围的点,从而形成有限维的表达。这就是信号在时域或空域的量化。

    这样做不可避免要丢失信息。但是,由于小邻域内信号的高度相关,信息丢失的程度往往并不显著。而且,从理论上说,这相当于在频域中的低通过率。对于有限能量的连续信号,不可能在无限高的频域中依然保持足够的强度,只要采样密度足够,丢失的东西可以任意的少。

    除了表示信号,对于几何形体的表达也经常使用量化,比如表示curve和surface。

  3. 找出有限个数充分表达一个对象也许不是最困难的。不过,在其上面建立数学结构却未必了。一般来说,我们要对其进行处理,首先需要一个拓扑结构用以描述空间上的点是如何联系在一起。直接建立拓扑结构在数学上往往非常困难,也未必实用。因此,绝大部分工作采取的方式是首先建立度量结构。一个度量空间,其度量会自然地诱导出一个拓扑结构——不过,很多情况下我们似乎会无视它的存在。

    最简单的情况,就是使用原始向量表达的欧氏距离(Euclidean distance)作为metric。不过,由于原始表达数值的不同特性,这种方式效果一般不是特别好,未必能有效表达实际对象的相似性(或者不相似性)。因此,很多工作会有再此基础上进行度量的二次建立。方式是多种多样的,一种是寻求一个映射,把原空间的元素变换到一个新的空间,在那里欧氏距离变得更加合适。这个映射发挥的作用包括对信息进行筛选,整合,对某些部分进行加强或者抑制。这就是大部分关于feature selection,feature extraction,或者subspace learning的文章所要做的。另外一种方式,就是直接调节距离的计算方式(有些文章称之为metric learning)。

    这两种方式未必是不同的。如果映射是单射,那么它相当于在原空间建立了一个不同的度量。反过来,通过改变距离计算方式建立的度量在特定的条件下对应于某种映射。

  4. 大家可能注意到,上面提到的度量建立方法,比如欧氏距离,它需要对元素进行代数运算。对于普通的向量空间,线性运算是天然赋予的,我们无须专门建立,所以可以直接进行度量的构造——这也是大部分工作的基础。可是,有些事物其原始表达不是一个n-tuple,它可能是一个set,一个graph,或者别的什么特别的object。怎么建立代数运算呢?

    一种方法是直接建立。就是给这些东西定义自己的加法和数乘。这往往不是那么直接(能很容易建立的线性运算结构早已经被建立好并广泛应用了),可能需要涉及很深的数学知识,并且要有对问题本身的深入了解和数学上的洞察力。不过,一个新的代数结构一旦建立起来,其它的数学结构,包括拓扑,度量,分析,以及内积结构也随之能被自然地诱导出来,我们也就具有了对这个对象空间进行各种数学运算和操作的基础。加法和数乘看上去简单,但是如果我们对于本来不知道如何进行加法和数乘的空间建立了这两样东西,其理论上的贡献是非常大的。

    (一个小问题:大家常用各种graphical model,但是,每次这些model都是分别formulate,然后推导出estimation和evaluation的步骤方法。是否可能对"the space of graphical model"或者它的某个特定子集建立某种代数结构呢?(不一定是线性空间,比如群,环,广群, etc)从而使得它们在代数意义上统一起来,而相应的estimation或者evaluation也可以用过代数运算derive。这不是我的研究范围,也超出了我目前的能力和知识水平,只是我相信它在理论上的重要意义,留作一个远景的问题。事实上,数学中确实有一个分支叫做 Algebraic statistics 可能在探讨类似的问题,不过我现在对此了解非常有限。)

  5. 回到我们的正题,除了直接建立运算定义,另外一种方式就是嵌入(embedding)到某个向量空间,从而继承其运算结构为我所用。当然这种嵌入也不是乱来,它需要保持原来这些对象的某种关系。最常见的就是保距嵌入(isometric embedding),我们首先建立度量结构(绕过向量表达,直接对两个对象的距离通过某种方法进行计算),然后把这个空间嵌入到目标空间,通常是有限维向量空间,要求保持度量不变。

    “嵌入”是一种在数学上应用广泛的手段,其主要目标就是通过嵌入到一个属性良好,结构丰富的空间,从而利用其某种结构或者运算体系。在拓扑学中,嵌入到metric space是对某个拓扑空间建立度量的重要手段。而在这里,我们是已有度量的情况下,通过嵌入获取线性运算的结构。除此以来,还有一种就是前些年比较热的manifold embedding,这个是通过保持局部结构的嵌入,获取全局结构,后面还会提到。

  6. 接下来的一个重要的代数结构,就是内积(inner product)结构。内积结构一旦建立,会直接诱导出一种性质良好的度量,就是范数(norm),并且进而诱导出拓扑结构。一般来说,内积需要建立在线性空间的基础上,否则连一个二元运算是否是内积都无法验证。不过,kernel理论指出,对于一个空间,只要定义一个正定核(positive kernel)——一个符合正定条件的二元运算,就必然存在一个希尔伯特空间,其内积运算等效于核运算。这个结论的重要意义在于,我们可以绕开线性空间,通过首先定义kernel的方式,诱导出一个线性空间(叫做再生核希尔伯特空间 Reproducing Kernel Hilbert Space),从而我们就自然获得我们所需要的度量结构和线性运算结构。这是kernel theory的基础。

    在很多教科书中,以二次核为例子,把二维空间变成三维,然后告诉大家kernel用于升维。对于这种说法,我一直认为在一定程度上是误导的。事实上,kernel的最首要意义是内积的建立(或者改造),从而诱导出更利于表达的度量和运算结构。对于一个问题而言,选择一个切合问题的kernel比起关注“升维”来得更为重要。

    kernel被视为非线性化的重要手段,用于处理非高斯的数据分布。这是有道理的。通过nonlinear kernel改造的内积空间,其结构和原空间的结构确实不是线性关联,从这个意义上说,它实施了非线性化。不过,我们还应该明白,它的最终目标还是要回到线性空间,新的内积空间仍旧是一个线性空间,它一旦建立,其后的运算都是线性的,因此,kernel的使用就是为了寻求一个新的线性空间,使得线性运算更加合理——非线性化的改造最终仍旧是要为线性运算服务。

    值得一提的是,kernelization本质上说还是一种嵌入过程:对于一个空间先建立内积结构,并且以保持内积结构不变的方式嵌入到一个高维的线性空间,从而继承其线性运算体系。

  7. 上面说到的都是从全局的方式建立代数结构的过程,但是那必须以某种全局结构为基础(无论预先定义的是运算,度量还是内积,都必须适用于全空间。)但是,全局结构未必存在或者适合,而局部结构往往简单方便得多。这里就形成一种策略,以局部而达全局——这就是流形(manifold)的思想,而其则根源于拓扑学。

    从拓扑学的角度说,流形就是一个非常优良的拓扑空间:符合Hausdorff分离公理(任何不同的两点都可以通过不相交的邻域分离),符合第二可数公理(具有可数的拓扑基),并且更重要的是,局部同胚于R^n。因此,一个正则(Regular)流形基本就具有了各种最良好的拓扑特性。而局部同胚于R^n,代表了它至少在局部上可以继承R^n的各种结构,比如线性运算和内积,从而建立分析体系。事实上,拓扑流形继承这些结构后形成的体系,正是现代流形理论研究的重点。继承了分析体系的流形,就形成了微分流形(Differential manifold),这是现代微分几何的核心。而微分流形各点上的切空间(Tangent Space),则获得了线性运算的体系。而进一步继承了局部内积结构的流形,则形成黎曼流形(Riemann manifold),而流形的全局度量体系——测地距离(geodesics)正是通过对局部度量的延伸来获得。进一步的,当流行本身的拓扑结构和切空间上的线性结构发生关系——也就获得一簇拓扑关联的线性空间——向量丛(Vector bundle)。

    虽然manifold theory作为现代几何学的核心,是一个博大精深的领域,但是它在learning中的应用则显得非常狭窄。事实上,对于manifold,很多做learning的朋友首先反应的是ISOMAP, LLE, eigenmap之类的算法。这些都属于embedding。当然,这确实是流形理论的一个重要方面。严格来说,这要求是从原空间到其映像的微分同胚映射,因此,嵌入后的空间在局部上具有相同的分析结构,同时也获得了各种好处——全局的线性运算和度量。不过,这个概念在learning的应用中被相当程度的放宽了——微分同胚并不能被完全保证,而整个分析结构也不能被完全保持。大家更关注的是保持局部结构中的某个方面——不过这在实际应用中的折衷方案也是可以理解的。事实表明,当原空间中的数据足够密集的情况下,这些算法工作良好。

    Learning中流形应用的真正问题在于它被过滥地运用于稀疏空间(Sparse space),事实上在高维空间中撒进去几千乃至几十万点,即使最相邻的几点也难称为局部了,局部的范围和全局的范围其实已经没有了根本差别,连局部的概念都立不住脚的时候,后面基于其展开的一切工作也都没有太大的意义。事实上,稀疏空间有其本身的规律和法则,通过局部形成全局的流形思想从本质上是不适合于此的。虽然,流形是一种非常美的理论,但是再漂亮的理论也需要用得其所——它应该用于解决具有密集数据分布的低维空间。至于,一些paper所报告的在高维空间(比如人脸)运用流形方法获得性能提升,其实未必是因为“流形”本身所起的作用,而很可能是其它方面的因素。

  8. 流形在实际应用中起重要作用的还有两个方面:一个是研究几何形体的性质(我们暂且不谈这个),还有就是它和代数结构的结合形成的李群(Lie group)和李代数(Lie algebra)。 当我们研究的对象是变换本身的时候,它们构成的空间是有其特殊性的,比如所有子空间投影形成了Grassmann流形,所有的可逆线性算子,或者仿射算子,也形成各自的流形。对他们的最重要操作是变换的结合,而不是加法数乘,因此,它们上面定义的更合适的代数结构应该是群和不是线性空间。而群和微分流形的结合体——李群则成为它们最合适的描述体系——而其切空间则构成了一种加强的线性空间:李代数,用于描述其局部变化特性。

    李代数和李群的关系是非常漂亮的。它把变换的微变化转换成了线性空间的代数运算,使得移植传统的基于线性空间的模型和算法到李空间变得可能。而且李代数中的矩阵比起变换本身的矩阵甚至更能反映变换的特性。几何变换的李代数矩阵的谱结构就能非常方便地用于分析变换的几何特性。

    最后,回头总结一下关于嵌入这个应用广泛的策略,在learning中的isometry, kernel和manifold embedding都属于此范畴,它们分别通过保持原空间的度量结构,内积结构和局部结构来获得到目标(通常是向量空间)的嵌入,从而获得全局的坐标表达,线性运算和度量,进而能被各种线性算法和模型所应用。

    在获得这一系列好处的同时,也有值得我们注意的地方。首先,嵌入只是一种数学手段,并不能取代对问题本身的研究和分析。一种不恰当的原始结构或者嵌入策略,很多时候甚至适得其反——比如稀疏空间的流形嵌入,或者选取不恰当的kernel。另外,嵌入适合于分析,而未必适合于重建或者合成。这是因为嵌入是一个单射(injection),目标空间不是每一个点都和原空间能有效对应的。嵌入之后的运算往往就打破了原空间施加的限制。比如两个元素即使都是从原空间映射过来,它们的和却未必有原像,这时就不能直接地回到原空间了。当然可以考虑在原空间找一个点它的映射与之最近,不过这在实际中的有效性是值得商榷的。

和Learning有关的数学世界是非常广博的,我随着学习和研究的深入,越来越发现在一些我平常不注意的数学分支中有着适合于问题的结构和方法。比如,广群(groupoid)和广代数(algebroid)能克服李群和李代数在表示连续变换过程中的一些困难——这些困难困扰了我很长时间。解决问题和建立数学模型是相辅相成的,一方面,一个清晰的问题将使我们有明确的目标去寻求合适的数学结构,另一方面,对数学结构的深入理解对于指导问题的解决也是有重要作用的。对于解决一个问题来说,数学工具的选择最重要的是适合,而不是高深,但是如果在现有数学方法陷入困难的时候,寻求更高级别的数学的帮助,往往能柳暗花明。数学家长时间的努力解决的很多问题,并不都是理论游戏,他们的解决方案中很多时候蕴含着我们需要的东西,而且可能导致对更多问题的解决——但是我们需要时间去学习和发现它们。

June 22

拓扑:游走于直观与抽象之间

近日来,抽空再读了一遍点集拓扑(Point Set Topology),这是我第三次重新学习这个理论了。我看电视剧和小说,极少能有兴致看第二遍,但是,对于数学,每看一次都有新的启发和收获。

代数,分析,和拓扑,被称为是现代数学的三大柱石。最初读拓扑,是在两三年前,由于学习流形理论的需要。可是,随着知识的积累,发现它是很多理论的根基。可以说,没有拓扑,就没有现代意义的分析与几何。我们在各种数学分支中接触到的最基本的概念,比如,极限,连续,距离(度量),边界,路径,在现代数学中,都源于拓扑。

拓扑学是一门非常奇妙的学科,它把最直观的现象和最抽象的概念联系在一起了。拓扑描述的是普遍使用的概念(比如开集,闭集,连续),我们对这些概念习以为常,理所当然地使用着,可是,真要定义它,则需要对它们本质的最深刻的洞察。数学家们经过长时间的努力,得到了这些概念的现代定义。这里面很多第一眼看上去,会感觉惊奇——怎么会定义成这个样子。

首先是开集。在学习初等数学时,我们都学习开区间 (a, b)。可是,这只是在一条线上的,怎么推广到二维空间,或者更高维空间,或者别的形体上呢?最直观的想法,就是“一个不包含边界的集合”。可是,问题来了,给一个集合,何谓“边界”?在拓扑学里面,开集(Open Set)是最根本的概念,它是定义在集合运算的基础上的。它要求开集符合这样的条件:开集的任意并集和有限交集仍为开集。

我最初的时候,对于这样的定义方式,确实百思不解。不过,读下去,看了和做了很多证明后,发现,这样的定义一个很重要的意义在于:它保证了开集中每个点都有一个邻域包含在这个集合内——所有点都和外界(补集)保持距离。这样的理解应该比使用集合运算的定义有更明晰的几何意义。但是,直观的东西不容易直接形成严谨的定义,使用集合运算则更为严格。而集合运算定义中,任意并集的封闭性是对这个几何特点的内在保证。

另外一个例子就是“连续函数”(Continuous Function)。在学微积分时,一个耳熟能详的定义是“对任意的epsilon > 0,存在delta > 0,使得 。。。。”,背后最直观的意思就是“足够近的点保证映射到任意小的范围内”。可是,epsilon, delta都依赖于实空间,不在实空间的映射又怎么办呢?拓扑的定义是“如果一个映射的值域中任何开集的原像都是开集,那么它连续。”这里就没有epsilon什么事了。

这里的关键在于,在拓扑学中,开集的最重要意义就是要传递“邻域”的意思——开集本身就是所含点的邻域。这样连续定义成这样就顺理成章了。稍微把说法调节一下,上面的定义就变成了“对于f(x)的任意领域U,都有x的一个邻域V,使得V里面的点都映射到U中。”

这里面,我们可以感受到为什么开集在拓扑学中有根本性的意义。既然开集传达“邻域”的意思,那么,它最重要的作用就是要表达哪些点靠得比较近。给出一个拓扑结构,就是要指出哪些是开集,从而指出哪些点靠得比较近,这样就形成了一个聚集结构——这就是拓扑。

可是这也可以通过距离来描述,为什么要用开集呢,反而不直观了。某种意义上说,拓扑是“定性”的,距离度量是“定量”的。随着连续变形,距离会不断变化,但是靠近的点还是靠近,因此本身固有的拓扑特性不会改变。拓扑学研究的就是这种本质特性——连续变化中的不变性。

在拓扑的基本概念中,最令人费解的,莫过于“紧性”(Compactness)。它描述一个空间或者一个集合“紧不紧”。正式的定义是“如果一个集合的任意开覆盖都有有限子覆盖,那么它是紧的”。乍一看,实在有点莫名其妙。它究竟想描述一个什么东西呢?和“紧”这个形容词又怎么扯上关系呢?

一个直观一点的理解,几个集合是“紧”的,就是说,无限个点撒进去,不可能充分散开。无论邻域多么小,必然有一些邻域里面有无限个点。上面关于compactness的这个定义的玄机就在有限和无限的转换中。一个紧的集合,被无限多的小邻域覆盖着,但是,总能找到其中的有限个就能盖全。那么,后果是什么呢?无限个点撒进去,总有一个邻域包着无数个点。邻域们再怎么小都是这样——这就保证了无限序列中存在极限点。

Compact这个概念虽然有点不那么直观,可是在分析中有着无比重要的作用。因为它关系到极限的存在性——这是数学分析的基础。了解泛函分析的朋友都知道,序列是否收敛,很多时候就看它了。微积分中,一个重要的定理——有界数列必然包含收敛子列,就是根源于此。

在学习拓扑,或者其它现代数学理论之前,我们的数学一直都在有限维欧氏空间之中,那是一个完美的世界,具有一切良好的属性,Hausdorff, Locally compact, Simply connected,Completed,还有一套线性代数结构,还有良好定义的度量,范数,与内积。可是,随着研究的加深,终究还是要走出这个圈子。这个时候,本来理所当然的东西,变得不那么必然了。

  • 两个点必然能分开?你要证明空间是Hausdorff的。
  • 有界数列必然存在极限点?这只在locally compact的空间如此。
  • 一个连续体内任意两点必然有路径连接?这可未必。

一切看上去有悖常理,而又确实存在。从线性代数到一般的群,从有限维到无限维,从度量空间到拓扑空间,整个认识都需要重新清理。而且,这些绝非仅是数学家的概念游戏,因为我们的世界不是有限维向量能充分表达的。当我们研究一些不是向量能表达的东西的时候,度量,代数,以及分析的概念,都要重新建立,而起点就在拓扑。

June 02

MATLAB 效率再议

关于MATLAB的效率问题,很多文章,包括我之前写的一些,主要集中在使用向量化以及相关的问题上。但是,最近我在实验时对代码进行profile的过程中,发现在新版本的MATLAB下,for-loop已经得到了极大优化,而效率的瓶颈更多是在函数调用和索引访问的过程中。

由于MATLAB特有的解释过程,不同方式的函数调用和元素索引,其效率差别巨大。不恰当的使用方式可能在本来不起眼的地方带来严重的开销,甚至可能使你的代码的运行时间增加上千倍(这就不是多买几台服务器或者增加计算节点能解决的了,呵呵)。

下面通过一些简单例子说明问题。(实验选在装有Windows Vista的一台普通的台式机(Core2 Duo 2.33GHz + 4GB Ram)进行,相比于计算集群, 这可能和大部分朋友的环境更相似一些。实验过程是对某一个过程实施多次的整体进行计时,然后得到每次过程的平均时间,以减少计时误差带来的影响。多次实验,在均值附近正负20%的范围内的置信度高于95%。为了避免算上首次运行时花在预编译上的时间,在开始计时前都进行充分的“热身”运行。)

函数调用的效率

一个非常简单的例子,把向量中每个元素加1。(当然这个例子根本不需要调函数,但是,用它主要是为了减少函数执行本身的时间,突出函数解析和调用的过程。)

作为baseline,先看看最直接的实现

% input u: u is a 1000000 x 1 vector
v = u + 1; 

这个过程平均需要0.00105 sec。而使用长期被要求尽量避免的for-loop

n = numel(u);
% v = zeros(n, 1) has been pre-allocated.
for i = 1 : n
    v(i) = u(i) + 1;
end

所需的平均时间大概是0.00110 sec。从统计意义上说,和vectorization已经没有显著差别。无论是for-loop或者vectorization,每秒平均进行约10亿次“索引-读取-加法-写入”的过程,计算资源应该得到了比较充分的利用。

要是这个过程使用了函数调用呢?MATLAB里面支持很多种函数调用方式,主要的有m-function, function handle, anonymous function, inline, 和feval,而feval的主参数可以是字符串名字,function handle, anonymous function或者inline。

用m-function,就是专门定义一个函数

function y = fm(x)
    y = x + 1;
在调用时
for i = 1 : n
    v(i) = fm(u(i));
end

function handle就是用@来把一个function赋到一个变量上,类似于C/C++的函数指针,或者C#里面的delegate的作用

fh = @fm;
for i = 1 : n
    v(i) = fh(u(i));
end

anonymous function是一种便捷的语法来构造简单的函数,类似于LISP, Python的lambda表达式

fa = @(x) x + 1;
for i = 1 : n
    v(i) = fa(u(i));
end

inline function是一种传统的通过表达式字符串构造函数的过程

fi = inline('x + 1', 'x');
for i = 1 : n
    v(i) = fi(u(i));
end

feval的好处在于可以以字符串方式指定名字来调用函数,当然它也可以接受别的参数。

v(i) = feval('fm', u(i));
v(i) = feval(fh, u(i));
v(i) = feval(fa, u(i)); 
对于100万次调用(包含for-loop本身的开销,函数解析(resolution),压栈,执行加法,退栈,把返回值赋给接收变量),不同的方式的时间差别很大:
m-function 0.385 sec
function handle 0.615 sec
anonymous function 0.635 sec
inline function 166.00 sec
feval('fm', u(i)) 8.328 sec
feval(fh, u(i)) 0.618 sec
feval(fa, u(i)) 0.652 sec
feval(@fm, u(i)) 2.788 sec
feval(@fa, u(i)) 34.689 sec

从这里面,我们可以看到几个有意思的现象:

  • 首先,调用自定义函数的开销远远高于一个简单的计算过程。直接写 u(i) = v(i) + 1 只需要  0.0011 秒左右,而写u(i) = fm(v(i)) 则需要0.385秒,时间多了几百倍,而它们干的是同一件事情。这说明了,函数调用的开销远远大于for-loop自己的开销和简单计算过程——在不同情况可能有点差别,一般而言,一次普通的函数调用花费的时间相当于进行了几百次到一两千次双精度浮点加法。
  • 使用function handle和anonymous function的开销比使用普通的m-函数要高一些。这可能是由于涉及到句柄解析的时间,而普通的函数在第一次运行前已经在内存中进行预编译。
  • inline function的运行时间则令人难以接受了,竟然需要一百多秒(是普通函数调用的四百多倍,是直接计算的十几万倍)。这是因为matlab是在每次运行时临时对字符串表达式(或者它的某种不太高效的内部表达)进行parse。
  • feval(fh, u(i))和fh(u(i)),feval(fa, u(i))和fa(u(i))的运行时间很接近,表明feval在接受句柄为主参数时本身开销很小。但是,surprising的是
    for i = 1 : n
        v(i) = feval(@fm, u(i));
    end
    比起
    fh = @fm;
    for i = 1 : n
        v(i) = feval(fh, u(i));
    end
    慢了4.5倍 (前者0.618秒,后者2.788秒)。
    
    for i = 1 : n
        v(i) = feval(@(x) x + 1, u(i));
    end
    比起
    fa = @(x) x + 1;
    for i = 1 : n
        v(i) = feval(fa, u(i));
    end
    竟然慢了53倍(前者0.652秒,后者34.689秒)。
    

    由于在MATLAB的内部实现中,function handle的解析是在赋值过程中进行的,所以预先用一个变量把句柄接下,其效果就是预先完成了句柄解析,而如果直接把@fm或者@(x) x + 1写在参数列上,虽然写法简洁一些,但是解析过程是把参数被赋值到所调函数的局部变量时才进行,每调用一次解析一次,造成了巨大的开销。

  • feval使用字符串作为函数名字时,所耗时间比传入句柄大,因为这涉及到对函数进行搜索的时间(当然这个搜索是在一个索引好的cache里面进行(除了第一次),而不是在所有path指定的路径中搜索。)

在2007年以后,MATLAB推出了arrayfun函数,上面的for-loop可以写为

v = arrayfun(fh, u)
这平均需要4.48 sec,这比起for-loop(需时0.615 sec)还慢了7倍多。这个看上去“消除了for-loop"的函数,由于其内部设计的原因,未必能带来效率上的正效果。

元素和域的访问

除了函数调用,数据的访问方式对于效率也有很大影响。MATLAB主要支持下面一些形式的访问:

  • array-index  A(i): 
  • cell-index:  C{i}; 
  • struct field:  S.fieldname
  • struct field (dynamic):  S.('fieldname')

这里主要探索单个元素或者域的访问(当然,MATLAB也支持对于子数组的非常灵活整体索引)。

对于一百万次访问的平均时间

A(i) for a numeric array 0.0052 sec
C{i} for a cell array 0.2568 sec
struct field 0.0045 sec
struct field (with dynamic name) 1.0394 sec

我们可以看到MATLAB对于单个数组元素或者静态的struct field的访问,可以达到不错的速度,在主流台式机约每秒2亿次(连同for-loop的开销)。而cell array的访问则明显缓慢,约每秒400万次(慢了50倍)。MATLAB还支持灵活的使用字符串来指定要访问域的语法(动态名字),但是,是以巨大的开销为代价的,比起静态的访问慢了200倍以上。

关于Object-oriented Programming

MATLAB在新的版本中(尤其是2008版),对于面向对象的编程提供了强大的支持。在2008a中,它对于OO的支持已经不亚于python等的高级脚本语言。但是,我在实验中看到,虽然在语法上提供了全面的支持,但是matlab里面面向对象的效率很低,开销巨大。这里仅举几个例子。

  • object中的property的访问速度是3500万次,比struct field慢了6-8倍。MATLAB提供了一种叫做dependent property的属性,非常灵活,但是,效率更低,平均每秒访问速度竟然低至2.6万次(这种速度基本甚至难以用于中小规模的应用中)。
  • object中method调用的效率也明显低于普通函数的调用,对于instance method,每百万次调用,平均需时5.8秒,而对于static method,每百万次调用需时25.8秒。这相当于每次调用都需要临时解析的速度,而matlab的类方法解析的效率目前还明显偏低。
  • MATLAB中可以通过改写subsref和subsasgn的方法,对于对象的索引和域的访问进行非常灵活的改造,可以通过它创建类似于数组的对象。但是,一个符合要求的subsref的行为比较复杂。在一个提供了subsref的对象中,大部分行为都需要subsref进行调度,而默认的较优的调度方式将被掩盖。在一个提供了subsref的类中(使用一种最快捷的实现),object property的平均访问速度竟然降到1万5千次每秒。

建议

根据上面的分析,对于撰写高效MATLAB代码,我有下面一些建议:

  1. 虽然for-loop的速度有了很大改善,vectorization(向量化)仍旧是改善效率的重要途径,尤其是在能把运算改写成矩阵乘法的情况下,改善尤为显著。
  2. 在不少情况下,for-loop本身已经不构成太大问题,尤其是当循环体本身需要较多的计算的时候。这个时候,改善概率的关键在于改善循环体本身而不是去掉for-loop。
  3. MATLAB的函数调用过程(非built-in function)有显著开销,因此,在效率要求较高的代码中,应该尽可能采用扁平的调用结构,也就是在保持代码清晰和可维护的情况下,尽量直接写表达式和利用built-in function,避免不必要的自定义函数调用过程。在次数很多的循环体内(包括在cellfun, arrayfun等实际上蕴含循环的函数)形成长调用链,会带来很大的开销。
  4. 在调用函数时,首选built-in function,然后是普通的m-file函数,然后才是function handle或者anonymous function。在使用function handle或者anonymous function作为参数传递时,如果该函数被调用多次,最好先用一个变量接住,再传入该变量。这样,可以有效避免重复的解析过程。
  5. 在可能的情况下,使用numeric array或者struct array,它们的效率大幅度高于cell array(几十倍甚至更多)。对于struct,尽可能使用普通的域(字段,field)访问方式,在非效率关键,执行次数较少,而灵活性要求较高的代码中,可以考虑使用动态名称的域访问。
  6. 虽然object-oriented从软件工程的角度更为优胜,而且object的使用很多时候很方便,但是MATLAB目前对于OO的实现效率很低,在效率关键的代码中应该慎用objects。
  7. 如果需要设计类,应该尽可能采用普通的property,而避免灵活但是效率很低的dependent property。如非确实必要,避免重载subsref和subsasgn函数,因为这会全面接管对于object的接口调用,往往会带来非常巨大的开销(成千上万倍的减慢),甚至使得本来几乎不是问题的代码成为性能瓶颈。
May 30

回望第一学年

考试周结束后,在MIT的第一学年落下帷幕了,接下来将是长达3个多月的长假。很多同学外出旅游,或者回家度假。而我将留在校园,继续我的research。

经过了一个学年的努力,最重要的目标终于达成了:通过TQE。

在MIT的研究生在学位答辩之前需要经过两次qualify,分别叫TQE(Technical Qualification Exam)和RQE(Research Qualification Exam),它们分别和课程与研究相关。每个系对于qualify有不同的policy,而对于Computer Science的学生来说,TQE并不是一次专门的考试,而是要求修四门主课,必须达到3A1B以上,才算通过TQE。为了保证足够的广度,系里把所开的全部主课分成三大领域(System, Theory, Artificial Intelligence),它们各自又分为三个子领域(共9个)。学生可以自由选四门主课列入自己的TQE Plan,但是这四门课程要求属于四个不同子领域,并且必须覆盖三大领域。

在这个学年里,我修的四门课,分别是

  • Machine Learning (AI)
  • Advanced Algorithms (Theory)
  • Advanced Computer Networks (System)
  • Natural Language Processing (AI,  本来是想选Advanced Computer Vision的,无奈Freeman教授这个学期取消了这门课程,只得换一门了)

在昨天,成绩全部出来了,以4A通过了这些课程,终于长舒了一口气,这意味着TQE的圆满完成,在接下来的日子可以专心做research了。

坦率地说,MIT CS的课程并不是特别艰深,只要认真去学,在这里的中国学生拿A并不是很难的事情。但是,很多课程确实很重,需要付出大量的时间和精力。为了这些课程,我也放弃了这一学年的主要学术会议了。

MIT的课程在国际上享有盛誉,因为大部分课程是通过OCW(http://ocw.mit.edu)向全世界公开的,可能不少朋友都曾经接触过。不过,在网上看公开的课程资料,和真正参与到这些课程中,感觉还是不一样的。就我个人感觉来说,这里很多课程的课程质量确实是一流的。其实从高中开始,我就有逃课的习惯,直到后来在科大读本科,以及在香港中文大学读硕士的时候,都大量缺课(这不见得是好事,不要随意模仿,呵呵~~~)。主要原因是感觉老师上课的节奏和我的思维节奏不是特别吻合——我更喜欢到图书馆自己看书。在MIT,老师从不点名,但是,我却极少缺课(除了临时的情况,缺过几堂)——老师所讲的课还是有很大的吸引力的。而且每堂课容量很大,我很少觉得老师讲得太慢了,甚至有些地方还未必能当时跟上,需要课后仔细回味。

我所上的这些课程都没有教科书,Lecture Notes都是授课老师(也可能包括他们的学生)自己编写,Problem Set也是老师自己设计。上课的教授都是在学术研究中非常有成就的学者,他们中很多都在顶尖学术会议和期刊上发表了大量paper。上课的内容也贴近学术研究的前沿。其中Advanced Computer Networks这门课,每堂课程的内容几乎都是近年发表的两三篇SIGCOMM的paper(这些paper有一半是MIT发表的)。有一些老教授上课时喜欢说类似这样的话:“今天讲的这个理论是我的某某学生提出的,当年他也和你们一样修我这门课”。

老师们在讲述一个理论,一个方法的时候,很鼓励学生去发现它们的问题和局限。在很多作业和考试题中,都有要求评论某个方法的缺点的题目。比较有意思的是Natural Language Processing的课,这门课每隔两三周,会布置一篇文章回去阅读(通常是发表在Science等顶尖刊物的经典paper),然后要求写一份评论,既要求写正面的,也要求写反面的。为了完成这种任务,我们回去只能把这篇文章反来复去看很多遍去分析其中潜藏的谬误,比如某个理论可能暗含了一些假设,一个表面完美的实验可能隐藏了不公平的因素。然后,大家会带着这些思考到课堂上讨论。

这些锻炼的影响是潜移默化的。上个月审了一些ECCV paper,我发现这种影响被带到了审稿过程中去了。以前审稿会或多或少使用诸如“方法不新”,“实验不充分”这类放之四海皆准,却没什么信息量的理由来批判一些文章。而这次,这类理由一条都没有用,而是逐条抽出文章中的谬误加以剖析批评。这样的评论更为深刻有力,不过,这些paper的处境可能也堪忧了。。。

CS大部分课程都有一个大的course project,而且占评分比重很大(一般在40% - 50%)。所有的project都是非命题的,从选择topic,survey,理论,实验,到最后的paper(report)都是学生自主完成。在topic选择完成后,一般需要先提交一份proposal,老师会给与指导意见。对于每个project,我还是尽己所能做到最好,平均在每个project上投入的时间和精力,绝不亚于一篇CVPR/ICCV。 这些project最后都得到了不错的评价,至于老师们对于project的实际要求有多高,这个我就说不准了。

这一年的压力是很大的,因为,这几门课的成败关系到能否顺利拿到PhD。不过,上课的过程非常有收获,也得到了很大的锻炼。除了上课,还投入相当多的时间学习数学(主要涉及泛函分析,优化理论,现代统计学,测度理论,平滑流行理论,李群论等,有一些是复习巩固,一些是新学),让自己的基础更加扎实。Research的过程拖得比较慢,但也有了一些重要的进展。

在香港的时候发了一些paper,觉得自己走在了领域的前端。而这一年让我重新变成一个学生:我还有很多东西不懂,需要继续学习。

May 24

编辑器漫谈

这篇文章谈到的话题——编辑器,和学术没有直接联系,可是却影响着我们每天的工作效率。我们平常使用电脑的过程中,至少有三类软件是必不可少的:操作系统,网络浏览器,以及编辑器。

网上的很多技术论坛充斥的着这样的争论:Windows vs. Linux,或者IE vs. Firefox。可是,这些争论虽然炽热,但其历史久远的程度却无法和下面的这个争辩相提并论:Vi/Vim vs. Emacs。熟悉Unix/Linux的朋友都知道它们的鼎鼎大名,这两款编辑器都诞生于1976年,其作者也分别是计算机界举足轻重的人物,Bill Joy (Sun的共同创始人和首席科学家),和Richard Stallman (GNU的创立者和自由软件的教父)。这两种编辑器代表了不同的设计哲学,自其诞生起,关于它们孰优孰劣的争论便无日无之,伴随着它们的成长,直到现在。在Wiki上有专门一个词条,叫做"Editor War",它描述了这场延续了超过30年依然没有尽头的“战争”。

在这篇文章中,我无意介入这场争论,而是以一个使用者的角度,回顾在我成长的不同阶段所观察到的各种编辑器的变迁。

遥远的Dos时代:WPS & CCED & Turbo IDE

在1994年的时候,我刚上初中,家里给我买了第一台电脑——那是一台486,使用MS-Dos 6.22 + Windows 3.1。虽然,Windows在那时已经初具规模,但是由于历史的原因,当时Dos下的软件远比Windows下面的丰富,因此大部分工作还是在Dos完成。

当时在国内,大家流行的是学习中文打字——尤其是五笔字型,由于相比于当时智能化程度还很低的拼音具有无可比拟的录入速度,广受青睐。国外的编辑器对于中文一般支持不好,而且不符合中国人的使用方式,使用的人不多。当时主流的文字处理方式是用WPS处理一般的文章,用CCED编撰表格。国内很多文章都是用20行20列的原稿纸撰写——一个方格一个字。这种排版方式只有WPS独家支持,而用Word实现非常困难而且使用不便。

一点题外话,这两款软件的作者,求伯君和朱崇君,是国内早期程序员的杰出代表。当时有一篇广为流传的报道——求伯君卖屋做软件,它让求伯君以及他创立的金山公司成为国产软件事业旗帜。除了WPS,新一代的电脑用户更加熟悉的金山词霸也是金山的作品。平心而论,WPS还是一款非常优秀的软件,也符合国内大部分用户的需要的。由于和微软商业竞争上的失策以及盗版的侵蚀,WPS跌下王座,辉煌不再,令人惋惜。

而程序的撰写,主要使用每种语言自己的IDE,比如Quick Basic, Turbo Pascal, Turbo C/C++,都有自己专用的集成编辑环境。这些软件一张1.44M的软盘就能装下,虽然功能远不如现在用DVD才能装下的Visual Studio,不过其设计确实非常经典,使用也很方便。

在那个时候Turbo Pascal/C的出品人Borland公司是程序设计领域的领袖。可是到了Windows时代,除了Delphi的短暂风行,已经根本无力和Visual Studio抗衡,在一系列战略失误之后,最近把它的程序设计软件事业(旗下的CodeGear公司),以两千多万美元的低价贱卖给一个别的公司。(两千多万美元,对于一般人来说听着很多,但是相比现在的IT业动辄十亿百亿的交易来说,这实在是惨淡得令人伤心。)除了Borland高层在战略上的把握连连失误之外,Borland的顶尖人才大量流失也是其失败的重要原因,连Turbo Pascal和Delphi的首席架构师,也在1996年被微软挖走,为微软开发了一种非常优秀的语言C#。

Windows时代:Microsoft独领风骚

在90年代中期开始,Word开始大量进入国内用户的视野,并且逐步取代了WPS,占据了文字处理的垄断地位。我最早使用Word,是在Word 5.0/6.0版的时候,当时相应的Office版本是4.3。到了Windows 95之后,办公套件的版本开始改用年代表示,到现在已经更新了多个版本:Office 95, 97, 2000, 2003, 2007。

Word 6.0给我的最印象深刻的体验就是所见即所得(WYSIWYG)——字体段落风格直接就显示在工作区里,虽然现在看来这是理所当然,这在当时可是个新概念,WPS等当时还是主流的软件都还不能实现这点,主要还是依靠特殊控制符来表达排版。当时,Office对我有着非常大的吸引力,包括Word/Excel/PowerPoint/Access,每个新版本出来都要详细学习一番。我家里的书柜中还摆放了不少这些软件的教程——不过已经很久没看了,它们是那个时代的印记。

不过,Word功能再强大,也是不能拿来写程序的。在Windows时代的程序开发,经历了短暂的战国时代后,由Microsoft的Visual Studio一统天下。在Windows刚出来的时候,那是一团混乱。习惯了Dos编程的程序员们完全不知道怎么在Windows下面写程序。仓卒之间,开发商就把Dos下的那些开发软件改改拿去出Windows下面的版本:比如Borland C++ 4.0/5.0和Microsoft C 7.0。由于这些东西本身不是为windows写的,用它们开发windows程序困难很多,而且很不稳定。混乱期结束后,真正的windows开发工具出来了,最著名的是Borland Delphi/C++ Builder和Microsoft Visual Studio(包括被广为使用的VC6和VB6)。

Borland和Microsoft的产品一度势均力敌,相持不下。随着它的一些顶尖设计师,包括Anders Hejlsberg被挖到微软,Visual Studio有了长足进步,对于Borland的产品有着越来越明显的优势。Microsoft依赖其强势的市场地位,开始推广.Net,并且推出了C#——这是Anders Hejlsberg领导下的一款力作,在很多方面都是非常优秀的。这时候Borland开始手足无措,左右摇摆,一时要搞Borland.Net来迎合新的趋势,却无法抗衡微软的先天优势,一时又要搞Kylix支持跨平台开发,却发现Linux市场太小无法养活自己,而且那里是开源的天下,商业软件不是那么受欢迎,Mac更是Apple的封闭王国,水泼不进。在进退失据之中逐步失去市场。

在很长一段时间,从高中开始,直到到去MIT之前,我一直是用Visual Studio作为自己的主要开发工具,经历了它从第一代版本VS97, VS6.0, VS.Net 2002, VS.Net 2003直到VS.Net 2005。功能强大而又体贴,是一款值得信赖的软件。我个人不是热别喜欢VS 6.0(虽然到了今天,它依然有大量的人在使用),主要是它对于ISO C++的标准支持得不是特别好,对于很多高级的模板用法编译上存在问题。但是,到了VS2003/2005,这些问题不复存在了。而对于C#的喜爱,也加深了我对于Visual Studio.Net的依赖。

专业领域:向文本回归

Windows是视觉的盛宴,所有的软件都热衷于“所见即所得”的方式——也给用户种下了“所见即所得”是理所当然的印象。在这种方式下,编辑成为了输入文字,以及通过大量的鼠标操作(interaction)来选择和调节样式的工作。Interactive的工作方式,简单,直观,对于普通用户来说确实是最好的。但是对于一些专业要求比较高的工作未必尽然。

从计算机科学的角度看来,它最大的问题在于内容和排版样式紧密耦合。这使得贯彻排版的一致性变得非常困难,基本需要每篇文章逐句逐段的调节。当需要更换排版方式的时候,整个耗时费力的排版过程需要重新进行。Word的样式表在部分解决了这个问题,但是,大量的细调依旧不可避免。

在很多专门领域,要求同一内容方便施加不同的排版方式(学术会议稿件),或者要求排版方式一致地贯彻在大量的文件之中(比如系统性的产生大批帮助文件)。这就需要内容和排版分离来大幅度提高效率——因为它要求内容和排版分离的编辑方式,而不是“所见即所得”的耦合方式。

首先是Latex。这是学术论文撰写的重要方式。它是以纯文本形式编写,以特殊指令,比如\section, \table, \equation等等来标示不同的内容,并通过style file来贯彻最后的排版样式。相比于Word,Latex产生的学术文档一般具有更高的排版质量。公式输入也更为方便(在熟练掌握主要的数学符号输入的情况下)。不过,坦白地说,虽然相比于Word,Latex对于内容和样式有更大程度的的分离,但是,样式还是在一定程度上耦合在所编辑的文本中,比如figure的位置和大小调节,以及\textbf, \emph这些直接写在文中而非style file中的指令。

Latex这样的编译式文字处理形式(以文本编辑源文件,并通过编译生成排版后的结果),还有着另外一个优点,便于自动产生。对于有大量图表的文章,我常用的一种方式就是写一个脚本把实验结果直接生成Latex图表,从而节省手工录入的过程,既提高了效率,又减少了错误。

真正的和排版无关的表达形式是XML,它单纯地放置内容,并且通过XSL(XML Stylesheet Language)来描述内容的表现方式。这种方式被大量运用于自动化的网页和文档的生成过程。我曾经在MATLAB Exchange发表过的一些Toolbox,后来就采用了这种方式大批生成帮助页。

http://web.mit.edu/dhlin/www/softwares/dmtoolbox_doc/helps/mdoc.dmtoolbox.mdir.xml

http://web.mit.edu/dhlin/www/softwares/sltoolbox_doc/helps/mdoc.sltoolbox.mdir.xml

大家可以到上面这个网址看看效果(index页面有时传输略缓慢)。主要是,撰写一些脚本从matlab源码的头注释中自动提取帮助内容,产生XML文件,并且通过XSL来定义表现形式。

文本如何产生呢?一种就是上述的用程序批量生成,还有很多还是要文本编辑器来编辑。在Windows里面,文本编辑基本上是百花齐放的局面。没有一种编辑器占垄断地位,我个人用得比较多的是UltraEdit,有时也用EditPlus和NotePad++。一般来说,一些专门格式的文件都用专门的编辑器编写,比如Latex,常用WinEdt或者LatexEditor;XML则用XML Spy,HTML主要用Visual Studio或者DreamWeaver。

在MIT的工作:文本编辑的世界

到了MIT——这是自由软件的大本营,工作方式进一步受到身边环境的同化,全面向Linux转移,这一年里Linux基本就是我的工作环境。(不要误解,这里从来不强迫大家使用什么操作系统,很多人还是坚持用windows的,呵呵,不过如果在公开邮件中宣传Windows可能会惹来Richard Stallman的责难)

在这里,我用Latex写作业,报告和paper,用Latex-Beamer来编写presentation(不再用ppt了),也和使用多年的Visual Studio说再见了。现在,软件的编译构建通过自行撰写MakeFile来完成。这是一种全新的体验——就是你能在这个过程中全面了解和控制每个过程运作的细节——这一切都是自己编写指令完成,而不再是隐藏在按钮后面的神秘过程。你可以非常自由而灵活地把握和定制流程的走向。

看上去有点麻烦,不过磨刀不误砍柴功,通过一个设计良好的脚本(这在一开始可能需要花点时间),可以把大量过程自动化(而使用传统工作方式,可能需要很多次操作),从而很大程度上提高工作效率,而且能很好保证过程的一致性和正确性。

这一次,都只是围绕着一个核心——编辑文本(无论是Latex source, Makefile, Bash script还是Program source code都是以文本形式存在的)。和Windows一样,Linux也有着无数的编辑器,但是有两种是长期占据优势地位的,就是文章一开始提到的Vi/Vim和Emacs。

在Windows里面,编辑器是普通的工具,在Linux里面,在相当程度上,这两种编辑器各自代表着一种文化潮流,其地位不是Windows那些编辑器可以相提并论的。Emacs诞生于MIT,Vi/Vim起源于Berkeley,在我身边的人中使用Emacs的占多数。这是一种强大得令人难以置信的编辑系统。它的使用说明长达561页,这仅仅是使用说明。还有一本长达300多页的说明,告诉你怎么用Emacs LISP来配置这个编辑器。

Emacs被很多人认为就是一个完备的“操作系统”。在这个编辑器里面,你除了编写各种格式的文件之外,可以构建和运行程序,收发邮件,以文本方式浏览网页(它以一种很聪明的方式把网页的内容以比较友好的文本方式呈现给你),还可以玩游戏,和煮咖啡(这个功能对于我来说暂时属于传说)。在某些自由软件社区,Emacs是一种信仰和生活方式,通过经过精心配置的Emacs,你基本上可以不离开它完成几乎全部工作(Hmm, 拿它来打3D游戏似乎还不现实,但是俄罗斯方块之类的是绰绰有余了,呵呵)。

我对于Emacs还没有那么忠诚,不过它在过去的这个学年确实是我的主要编辑工具,我用它完成了我全部的course project——它被用来撰写C/C++, Python, Java, 和Latex。但是,MATLAB还是用matlab自带的editor来写——值得一提的是MATLAB的editor有一种Emacs模式。

Emacs其实是一个非常灵活的软件,对非常核心的部件都可以全面定制,而远不仅仅是key mapping, color scheme等表面的东西。对它的配置,使用一种专门为它设计的语言,叫做Emacs LISP,这是一种基于LISP的专用语言。但学习起来,可能不那么容易。Emacs的配置过程颇为繁琐,它主要通过.emacs文件配置,大家可以在网上看看那些被分享的.emacs配置文件,基本都在数百行以上。也就是说,在让你的Emacs编辑器完全符合你的心意之前,需要编写一个几百行的配置脚本。当然,你用默认的配置也可以,不过在某些格式的文件的编写中没有那么好使。我在第一次使用前,花了整整三天才把它配得比较顺手了。当然了,第一次配好后,把.emacs存下来,以后就省心了——即使重装系统,把那个.emacs拷贝过来就行。

Emacs给人的感觉是有点过于重量级了,即使在一台配备Quadcore(3G Xeon x 4) + 8G Ram的工作站上,打开大部分插件时,还是有时感到一点delay。不要小看这一点delay,在代码编辑的过程中,在效率上会带来不可忽略的开销,(想想击键时需要等待0.2秒的感觉)。后来,始终觉得Emacs不太适合进行轻量级的编辑,于是转向Vim。

Vim也是一种非常优秀和强大的编辑器,编辑效率比Emacs更高。Emacs通过Ctrl, Alt, Shift这些辅助组合键来触发各种功能。而Vim则采用模式方式,它有多种mode。在Insert mode里面,就像普通编辑器那样,比如输入dd,就是敲进去两个字符dd;而在normal mode里面,dd就是删除当前行。它在编辑命令上下了很多的功夫,编辑功能特别强大。比如单是删除就有非常多的方式,dw 删除一个单词,d3w 删除三个单词,d4j 删除接下来三行,d5k 删除前面五行, dG 删除到文章末尾,d$删除到行尾,dft删除到下一个出现的字母t,d'a删除到下个书签为a的地方。。。。。。还有其它非常多样的编辑方式。这使得很多以前需要敲很多键才能完成的功能,在几个键内迅速实现。不过Vim的学习曲线比较陡峭,上手比较难,需要反复大量联系才能熟练,很多人看着几个模式来回切换就晕,更不用说用说那些千变万化的命令组合。不过经过一段时间的坚持练习,后面的编辑速度可以成倍提高,非一般编辑器能比。

相比于Emacs, Vim相对较轻(虽然也在变得臃肿之中),而且很多必要的部件本身就配置得不错,人工配置的工作量比较少。它也有一门专门的语言来配置,叫做Vim Script,新版的Vim也可以用python来配置了。对于我自己使用感觉来说,它的Line Number Display和Omni Completion是相比于Emacs更出色的亮点,而且编辑上感觉不到延时,也不需要老是把手指伸到Ctrl, Alt这种地方去了。

 

文本编辑是计算机一个很基础的用处,以至于我们常常忽略它。不过,当你深入到它的世界,却可以发现它独特的魅力。

May 21

哀悼同胞

随着最后一个project的完成,第一学年结束了。

这不是一个值得庆祝的时刻——当数万无价的生命无可挽回,当无数的同胞仍在失去亲人的痛苦之中,举国同哀的时候。

对于中华民族来说,2008年是不平凡的一年,我们共同经历了最严峻的挑战。在过去的半年里,我从未如此强烈地感觉到自己对于这片养育了自己的土地的血浓于水的情感。即使到了期末最紧张的时候,依然长时间地看着一篇篇或令人沉重,或令人感动的报道而不能自拔。

身在他乡,方知故乡情重。

虽然无法提供更多的帮助,在这里的大家还是尽了努力,向灾区的人民表达了自己的心意——虽处万里之遥,我们感同身受。

此时此刻,最想要表达的,就是对遇难同胞最深的哀悼。

愿,逝者安息,生者平安。