原文

机器学习与逻辑编程

(声明:本文内容纯属个人的技术兴趣,与本人在职公司的立场无关)

你可能没有想到,机器学习(machine learning)和逻辑编程(logic programming)有着一种美妙的关系,在我眼里她们就像一对亲姐妹。

很多人都了解机器学习,可是很少有人理解逻辑编程。在这篇短文里,我试图告诉你逻辑编程是什么,以及它与机器学习的相似之处。

逻辑编程是什么

说到逻辑编程(logic programming),人们不禁想到 Prolog 之类晦涩的逻辑式编程语言。很多人上本科的时候被迫学过 Prolog,但从来不知道它有何意义。毕业之后再听到 logic programming 这个词,就只剩下敬畏和茫然,或者觉得是没用的老古董。

其实逻辑编程是很美的东西,并不过时。它的有些思想已经悄悄被应用到了最先进的编程语言之中。逻辑编程的原理可以被很轻松的解释清楚,而不需要理解 Prolog。

最近研究机器学习,我发现逻辑编程与机器学习之间有着有趣而隐秘的关系。我希望这可以调动起你的胃口来。

要理解逻辑编程是什么,你只需要看一个很简单的例子:

有一个未知数 X,我们不知道它是多少,但我们知道:

​ X + 2 = 5

请问 X 是几?

以上求解 X 的问题就是一个逻辑程序。像 Prolog 这样的逻辑语言系统,会给你结果:X 等于 3。可是这个问题却不能用其它几乎所有编程语言来表达(C, C++, Python, Java, Go, Scala, Haskell, Rust, Swift…)。原因在于,使用普通的编程语言,你不能把“未知数”当成一个值来进行演算。

在我们的例子里面,X 的值是未知数,所以当普通语言看到 X + 2 这样的表达式,它就无法运行。它会报错:使用未初始化的变量 X。也就是说,你必须先知道 X 的值,你才能说 X + 2

但在像 Prolog 这样的逻辑式语言里面,“未知数”是可以被作为一个正常的值来进行计算的。它们可以被传递到其它函数里,可以被放进数据结构,可以进行复杂的逻辑组合操作,就像你在操作一个普通的数字或者字符串一样。

逻辑式程序中一般会有一个(或者多个)“目标”(goal)。目标一般是一个判断表达式,也就是说它的值是布尔类型(boolean)。这里我们的例子里只有一个目标,就是“X + 2 = 5”。也就是说,我们想要 X 加上 2 等于 5。

当逻辑式语言看到了目标,就把目标记下来。最后程序员开始提问:X 是几?这时候,逻辑语言的运行系统开始进行“反向计算”,找到未知数的值,使得目标的值为“真”(true)。在我们的例子里,系统会告诉你:X 等于 3。

为什么叫做“反向计算”呢?因为

  1. 我们先声明了未知变量 X
  2. 然后我们提出目标 X + 2 = 5

对于复杂一点的程序,1 和 2 之间可能还有其它的代码。我们最后的问题,却是问最开头声明的变量 X 等于几,所以系统从最后面的目标 X + 2 = 5 出发,“反向”推导出 X 的值。

这就是为什么研究逻辑式编程的人把这种操作叫做“反向计算”。你可能注意到了,我们的代码里面只写了加法(+)操作,而系统实质上为我们做了减法:5 - 2 得到 3。

如果你想深入理解逻辑式编程,我建议你看看 Dan Friedman 的书『The Reasoned Schemer』。但目前你了解到的这些,应该足以读完这篇文章。

机器学习与逻辑编程的相似点

你可能已经明白了逻辑编程是什么。下面我们来看看它跟机器学习有什么关系。

首先我们看到逻辑编程有“目标”(goal),比如 X + 2 = 5。在机器学习中有一个对应的东西,那就是误差函数(loss function)。只不过逻辑编程的 goal 是个等式,而机器学习的 loss function 是个函数。

逻辑编程系统会为你选择未知数的值,从而精确地“满足”这个 goal。而机器学习的目标呢,是要为你选择未知数的值,最小化这个 loss function,使得误差最小。看到相似之处了吗?所以,机器学习可以被看成是“在连续空间中的近似的逻辑编程”,而逻辑编程可以被看成是“在离散空间中的精确的机器学习”。

逻辑编程有“反向计算”,机器学习有“反向传递”(back propagation),而它们的工作方式,有着惊人的相似之处。只不过机器学习因为是连续空间的,所以需要使用微积分的原理,而不只是简单的逻辑组合。

实际上逻辑编程必须先进行正向计算,构造出含有未知数的结构,然后进行所谓“resolution“,求出未知数的值。而机器学习也类似,你必须进行一遍正向计算(forward pass),然后才能进行 back propagation,求出导数,并且更新“weight”的值。

逻辑编程的“未知数”(比如 X),对应了机器学习的 weight。实际上,机器学习的 weight 本质就是“未知数”。你需要得到它们的值,使得 loss function 最小,但一开头你不知道它们是什么,所以你给它们一些随机的初始值,让系统开始正向计算。机器学习的 weight 和逻辑编程的未知数如此的相似,它们可以被作为普通的值,与输入进行计算操作(Conv 等操作),直至你遇到 goal 或者 loss function,然后你掉头回去调整未知数的值……

机器学习框架是程序语言

所以呢,你现在明白了我为什么对机器学习感兴趣了吧。我看到了它与编程语言的优雅知识之间的联系,看到了它是对于“计算”概念的一种扩展。机器学习把“计算”和“微积分”有趣地融合在了一起。

实际上,你可以把机器学习的各种框架(framework)看成是新的编程语言,它们的工作原理类似于 Prolog 语言的运行时系统。如果要起一个名字,你也许可以把它们叫做“可求导编程语言”(differentiable programming language)。

写 framework 的工作,实质上是设计编程语言或者解释器,编译器。而有些 framework 所谓的“计算图”,实质就是编译器中的 data-flow graph 或者 control-flow graph 一类的东西。

目前这些语言还处于初级阶段,表达力比较弱,有各种不完善的地方。由于机器学习解决的是连续的数值问题,机器学习的“模型”一般要很简单才行,否则很可能出现学习不收敛的情况。所以我还不知道编程语言的很多概念能否顺利的迁移到机器学习上面。

但目前看来有一些很明显的对应关系和发展趋势:

  1. Feed-forward 网络,比如 CNN 一类的,对应了编程语言中最简单的表达式,或者叫“纯函数”。其中没有递归,也没有副作用。它只能处理图像一类具有固定长度的数据。
  2. RNN 对应的是程序语言里含有递归(循环)的函数。由于递归函数对应的是“递归数据结构”,这就是为什么 RNN 可以处理文本一类没有固定长度的“链表”数据。
  3. Neural Turing Machine 及其后续的研究 Differentiable Neural Computer,试图把更广阔的编程概念引入到机器学习里面,实现任意复杂的数据结构和计算。

由于数值计算特有的误差问题和机器学习带来的巨大计算量,我不知道这个趋势最终可以发展到什么地步。但编程语言和机器学习的这个联系,是优雅而让人回味的。

(如果你觉得这篇文章有启发,可以点击这里付费