mmap.page

人工智能

预备知识

  1. 初中程度的数学知识(主要是概率论)
  2. 基本的编程知识

潜艇和鱼

机器会思考吗? 这和问「潜艇会游泳吗」差不多。

-- Dijkstra (1984)

螺旋桨的转动,作用于水体,水体的反作用力推动潜艇游泳。 这就是潜艇游泳的原理,非常简单。 事实上,工程上的实作也很简单。 潜艇之所以复杂,是因为作为军用船舶有额外的需求, 比如老式潜艇通过双桨对转抵消反扭力来避免潜艇偏转, 现代潜艇通过自动控制技术来纠正潜艇偏转。 如果忽略这些需求,只是造会游泳的潜艇的话,并不复杂。

但是造机器鱼就麻烦了。 实际上,鱼游泳的机制我们并不是很清楚。 直到 2015 年,才有对鱼游泳机制(水体和鱼体的动量平衡)的量化研究发表, 而且这一研究仅限于鱼游泳的两种情形: 平稳前进和C型逃脱(鱼身迅速弯成C形,然后向外翻转快速游动)。

如果说潜艇是在流体力学还没有搞清楚鱼游泳机制的时候, 另辟蹊径造出游泳的机器, 那么人工智能,就是试图在脑科学还没有搞清楚人类思考机制的时候, 另辟蹊径造出思考的机器。

逻辑和计算

潜艇是怎么在鱼游泳的机制还没有搞清楚的时候造出来的呢? 鱼游泳具体的机制当时并不清楚(直到现在也不是很清楚), 但是鱼是通过水的反作用力游动的,这是比较容易观察到的。 那么,如果我们能找到什么东西给水施加作用力,就可以通过水的反作用力驱动潜艇了。 有什么东西可以给水施加作用力呢? 当时最常见的就是船桨和螺旋桨了。 由于潜艇并非依靠人力驱动,那螺旋桨就明显比船桨合适了。 所以,用螺旋桨就可以让潜艇游泳了。

同样,人思考的机制并不清楚,但人的思考,有一个特征是很容易观察到的。 那就是人的思考是通用的。 那么,有什么东西是通用于各个领域的呢? 最容易想到的就是逻辑。

什么是逻辑? 欧几里得的几何学是从公理(命题)出发,构造证明得到其他定理(命题), 构造证明所依据的规则是逻辑。 做出证明是困难的,但是已经构造出证明,验证证明的正确性则是容易的 (验证实际的证明并不是那么容易,但这只是因为实际的证明几乎总是包含一些隐含知识并省略一些中间步骤)。

如果我们接触过静态类型的程序语言,那么我们发现静态类型语言的类型系统和逻辑简直是双胞胎:

从参数(类型)出发,构造函数得到返回值(类型), 构造函数所依据的规则是类型系统。 构造函数是困难的,但是已经构造出函数,验证函数的类型正确是容易的 (它是如此容易,以至于我们不需要手工验证,编译器会自动验证)。

没错,它们确实是一对双胞胎,这对双胞胎的名字叫柯里-霍华德同构(Curry-Howard correspondence).

柯里-霍华德同构意味着计算(computing)和逻辑是等价的, 换句话说,任何逻辑能表达的东西,都能转化成计算。 而逻辑看起来是通用于各个领域的,那也就是说,各个领域的问题都能转化成计算。 看起来我们离人工智能已经很接近了。 事实上,80 年代确实出现了人工智能的热潮, 日本还号称要举国制造「第五代计算机」,发展 Prolog 之类的逻辑式编程语言。

理论上,我们只需输入一些知识,然后问逻辑式编程语言一些问题,就能自动获得答案。 但现实并没有那么美好。 想想看,我们要预测明天的天气,需要输入哪些知识? 如果仅仅输入历史数据、当前卫星云图、设备监测到的数值, 逻辑编程语言并不能演算出答案。 我们还需要输入如何从输入数据演算答案的知识。 也就是说这些如何从输入数据演算答案的知识才是天气预测程序的关键, 而不是逻辑式编程语言自带的那些逻辑规则!

像预测天气这样一个具体的应用问题都很难有效地转化为逻辑演算, 可想而知,要将人类的通用思考转化为逻辑运算就更难了。

所以最后这条路并没有走通。 这条道路后来被称为强人工智能。

当然,强人工智能虽然失败了, 但柯里-霍华德同构启发了以 Coq 为代表的自动化定理证明辅助工具, 并顺便解决了自然语言证明几乎总是包含隐含知识和跳过中间步骤的问题。 而逻辑式编程的思想也影响了另一些编程语言的设计。

搜索和解析

既然强人工智能这条路走不通, 那就不奢求建造一个通用的系统,并把各个领域的问题转化为这个通用系统能理解的形式。 转而针对专门领域建造专门的系统,应该要容易点吧? 这条路,被称为弱人工智能。

在工程上,当我们不奢望借助一个通用的框架来解决某个专门的问题时, 那往往一些看起来比较「笨」的做法就能取得不错的效果。

比如,如何设计一个智能客服系统?

让我们先退回到需求层面。 之所以有智能客服的需求,是要削减雇佣人工客服的开支。 然后我们反过来想, 假设世界上不存在智能客服系统,那该怎么做才能减少雇佣人工客服呢? 很简单,让客户自己回答自己的咨询。

通常的做法是事先准备好一些常见的问题和相应的答案, 当客户需要联系客服时,先提供 FAQ 页面的链接。 当常见的问题越来越多,一个页面放不下以后,就扩展这个系统, 将一个 FAQ 页面扩展成大量页面组成的知识库。 然后提供一个搜索功能,让客户能够快速定位到相关的问题。

看,实际上我们不一定需要一个智能客服系统, 我们需要做的是维护一个知识库(问答集),然后设计一个搜索系统。 而搜索系统的设计和实作,已经是非常成熟的经典问题了。 当然现代的搜索系统还在发展,但这主要是为了应对规模和性能的挑战。

实际上,很多预期客户擅长或习惯搜索的企业, 就是这样做的, 先让客户去知识库搜索,如果不能自行解决,再联系人工客服。

好了,减少人工客服的需求解决了。 可是我们不能光考虑削减成本,还得考虑客户的体验。 从体验的角度,先去搜索知识库,再联系客服,是两步,当中需要经过许多页面跳转, 这对客户是不友好的。 特别是,客户之前可能已经尝试通过自己的经验来解决遇到的问题,可是失败了, 尝试和失败的过程很可能已经消磨掉客户的耐心了。

我们注意到,搜索知识库,需要在搜索框里输入关键词, 而联系客服,需要在对话框或工单的文本框里输入问题。 那这两步是可以并成一步的。 也就是说,直接让客户输入问题, 然后系统从问题中提取出关键词,在后台进行搜索,将匹配到的结果显示在这个框下面。 如果客户发现提示的结果符合自己遇到的问题,就可以点击链接查看解决方案。 否则客户就继续输入问题描述或者直接提交问题给人工客服。

这里的提取关键词,可以先尝试最简单的文本匹配,在问题中匹配关键词。 我们看到,这背后并没有用什么人工智能的技术, 但从客户的角度来说,可能已经会有一点这个系统比较智能的感觉了。 事实上这个设计的名字恰恰是智能提示。

但是,别忘了,我们前面提到的一个假设「预期客户擅长或习惯搜索」。 这个假设可不一定成立。

实际上,很多业务的大多数客户, 往往不具备将自己遇到的问题提炼成一个包含关键词的简短语句的能力或耐心。 这时候智能客服系统要尝试分析客户含混的描述, 当分析失败或分析的结果不够好时,要尝试引导客户改进对问题的描述。

这叫做自然语言处理。

前面的智能提示把问题转化成现成的搜索问题, 这里的自然语言处理,我们也想把它转化成现成的解析问题。 代码只是一些字符串,之所以能运行,是首先被解析成了解释器或编译器能理解的结构, 然后交给解释器或编译器来解释或者编译。 同理,自然语言也是一些字符串,如果我们能将它解析成一个包含关键词和权重的结构, 也就是一个搜索引擎能理解的结构,那就可以交给搜索引擎来搜索答案。

看起来我们离结果已经很接近了。 编程语言的研究、设计和发展催生了很多解析相关的技术。 我们只需要将这些技术推广到自然语言上去就可以了。

但现实很残酷。 几百行代码就可以完成类似 lisp 这样容易解析的程序语言的解析器, 而像 Ruby 这样不容易解析的程序语言,几百行代码可能只完成了万里长征的第一步,词法分析(将字符串切成小块)。 看起来差别很大呀。但放到自然语言面前根本就不够看。 在自然语言面前,这根本算不上差别。

自然语言的解析是如此的复杂,以至于解析的第一步,最初级的词法分析都搞不定。 没办法,光靠各种解析的技术是搞不定了,只能通过别的方法猜出一个成功率较高的结果了。

拟合和马尔可夫性质

那我们该怎么猜?

想到猜,你脑子里第一个反应是什么?

女人的心思真难猜。

实际上猜女人的心思意义很大。 很多女人偏好网购,商家都想猜透女人的心思,有针对性地投放更精准的广告。 但这个问题也比较复杂,而且还要避免过于精准反而引起不安导致反效果。 我们还是找一个简单点的例子。

那么猜谜呢?

猜谜,谜面是自然语言,谜底是自然语言, 这根本还是自然语言处理的问题嘛。 况且谜面和谜底的联系是不符合自然语言的日常模式的(否则就太好猜了)。 也就是说这可能还是自然语言处理里难度比较高的问题。 所以我们也不讨论这个例子。

咦,猜下一个数字,这个问题应该足够简单了吧。 确实,IQ 测试和很多招聘笔试中经常有给出一个数列中的若干项,猜下一项是什么的题目。 我们知道,计算机非常擅长处理数字。所以这很可能是一个相对简单的问题。

该怎么教计算机猜数字呢? 先想想我们人类是如何猜数字的呢?

人类猜下一个数字,更多的是依靠一种直觉,或者说对数字的感觉, 这也是 IQ 测试和招聘笔试想要测试的。 但是显然我们没法把这种直觉教给计算机, 事实上我们想把这种直觉教给我们的同类都非常非常困难。

那还有什么猜的办法呢? 有一个常用的办法,就是把数列看成一个以自然数为定义域的函数, 将已知的数字转化成在笛卡儿坐标系中的点。 然后我们尽量以一条曲线把这些点连起来。 最后我们看看这条曲线像哪种函数的曲线, 构造那种函数的一个例子,比较一下构造出来的曲线和我们描出来的曲线, 逐渐调整函数的定义,使构造函数的曲线贴合描出来的曲线, 也就是贴合已知的数字。 这个过程叫做拟合(curve fitting)。

因此,我们只要让计算机去拟合就可以了。 那么,计算机该怎么去拟合呢? 最简单的就是暴力搜索,把已知的函数一个一个往上套。 对付 IQ 测试,这个暴力的方法可能已经足够好了。 但这只是因为 IQ 测试的题库不够大而已。 实际情况并不像 IQ 测试那么简单。 比如这些数字可能是某个实验观测得到的结果,可能并没有什么题库可以套。 那暴力搜索显然是行不通的。

那怎么办呢? 我们可以尝试增加一些限制,来减少一些歧路(剪枝)。 比如,我们可以假设,数列中的任何一个数字,只取决于它前面的一个数字。 然后我们就能以两个为一组进行猜测了, 这样的话,我们尝试拟合的相邻项函数,是一个一元(单参数)函数,这就大大缩小了范围了。 更重要的,因为只取决于前面的一个数字,而不用去考虑更前面的数字, 那我们可以直接把每一组数字分别传给不同的处理器跑,而不用把整个数列传过去, 当已知数字很多的时候(现实不是 IQ 测试题,数据可能会非常非常大), 这样分布式地拟合能大大加快进度。

我们尝试将这个猜数字的思路推广到更一般的情况, 我们把这些数字抽象成表示某个状态的数学结构(程序语言中的数据结构), 相应地,我们把猜下一个数字抽象成猜测下一个状态发生的概率。 于是我们就得到了概率论中的马尔可夫性质(Markov property).

当然,这个假设可能太强了,导致有些数列难以拟合。 比如斐波那契数列,完全不适用「只取决于它前面的一个数字」这个假设。 那我们可以扩展马尔可夫性质。 对斐波那契数列而言,我们可以构造一个新的序列, 序列的每个成员是一对(pair)斐波那契数列的相邻项:

haskell
[1, 1, 2, 3, 5, 8, 13 ..] # 斐波那契数列
[(1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13) ..] # 新序列

显然,这个新序列满足马尔可夫性质, 相应地,斐波那契数列是一个二阶马尔可夫数列。

类似地,我们可以构造一个新序列, 这个新序列的每个元素是由 n 个原序列元素组成的元组(tuple), 这样我们就将马尔可夫性质推广到 n 阶。 基于计算复杂度的考虑,工程上常用的是 3 阶马尔可夫结构。

总结一下猜的大体思路:

  1. 收集、积累大量数据
  2. 将这些数据表示为便于猜的数学结构(数据结构)
  3. 基于上一步骤得到的结构进行拟合
  4. 适当化简以降低计算复杂度

统计数据,表示数据,然后适当化简,拟合数据,去猜一个概率较大的答案,这叫做机器学习(machine learning)。

概率和贝叶斯定理

上面我们提到了机器学习是通过拟合来猜测答案,实际上,机器学习同时也通过猜测的答案来更好地拟合。

考虑过滤垃圾邮件这个例子。 一方面我们统计邮件中某些词语在正常邮件和垃圾邮件中出现的频率, 根据两者的不同推断一封新邮件是正常邮件还是垃圾邮件, 另一方面,我们又利用推断的结果反过来推断某些词语在正常邮件和垃圾邮件中出现的频率。 这样,这个系统分类的邮件越多,识别效果就越好。

抽象一下, 我们根据历史数据统计某一特征 f 在分类 S 和 N 中出现的概率 (pSucc f SpSucc f N), 据此推断新数据属于 S 还是 N. 同时,我们根据新数据的分类, 推断分类为 S 和 N 的数据具有特征 f 的概率(pSucc S fpSucc N f), 并进而调整 pSucc f SpSucc f N 的值。

那么这个学习过程其实是辗转计算pSucc f SpSucc S f这一组概率 (N的情况同理)。

pSucc f SpSucc S f有什么关系呢?

我们尝试代入一个具体的例子来摸索一下。 假设f代表邮件中出现了「正规发票」这个词, 而S代表这封邮件是垃圾邮件。 那么,直觉上,我们有:

  1. 包含「正规发票」这个词的邮件是垃圾邮件的概率越大,相应地,垃圾邮件中包含「正规发票」这个词的概率也越大
  2. 包含「正规发票」这个词的邮件是垃圾邮件的概率和垃圾邮件中包含「正规发票」这个词的概率并不相等
  3. 包含「正规发票」这个词的邮件是垃圾邮件的概率大于垃圾邮件中包含「正规发票」这个词的概率

抽象上述表述,我们得到:

haskell
pSucc f S = coefficient * (pSucc S f) -- coefficient > 0
pSucc f S /= pSucc S f
pSucc f S > pSucc S f

一般地,fS可以认为是两个先后发生的事件, pSucc f S可以认为是f发生后S发生的概率(这叫做条件概率或后验概率), pSucc S f同理。

交换fS, 我们得到:

haskell
pSucc S f = coefficient * (pSucc f S) -- coefficient > 0
pSucc S f /= pSucc f S
pSucc S f > pSucc f S

我们看到,前 2 条没问题,第 3 条出现了矛盾。 这说明前面我们抽象不当,fS并不是一般的两个事件, fS还具有一些我们没有考虑的隐含性质。

回头看我们的直觉:

包含「正规发票」这个词的邮件是垃圾邮件的概率大于垃圾邮件中包含「正规发票」这个词的概率

这一条背后其实有一个假设:

碰到包含「正规发票」这个词的邮件的概率要小于碰到垃圾邮件的概率。

也就是说,p f < p S.

假设p f不为 0, 稍加变形,我们得到 (p S) / (p f) > 1.

然后我们再将fS抽象为一般事件,那么我们可以用(p S) / (p f)来表示

haskell
pSucc f S = coefficient * (pSucc S f)

中的系数coefficient,这样,当p f < p S的时候, 就有pSucc f S > pSucc S f, 反之,当p f > p S的时候,pSucc f S < pSucc S f.

这样我们就得到了pSucc f SpSucc S f的关系:

haskell
pSucc f S = ((p S) / (p f)) * (pSucc S f)

显而易见,互换fS没问题(假设p S不为 0)。

既然fS是一般事件,我们不妨用ab替换它们,以凸显其一般性:

haskell
pSucc a b = ((p b) / (p a)) * (pSucc b a)

贝叶斯在 18 世纪发现了上面这个公式,因此它被称为贝叶斯定理(Bayes' theorem). 之所以称为定理,是因为贝叶斯是基于条件概率的定义推导出这个公式的。 我们认为贝叶斯定理足够简单,因此跳过条件概率的定义直接讲贝叶斯定理。

回到垃圾邮件过滤的问题,前面我们提到:

一方面我们统计邮件中某些词语在正常邮件和垃圾邮件中出现的频率, 根据两者的不同推断一封新邮件是正常邮件还是垃圾邮件,

这和贝叶斯定理有些差距。 也就是说,实际上我们没有直接统计p f,而是分别统计了pSucc N fpSucc S f. 由于我们的分类系统不考虑无法判定的情况,因此一封邮件要么是正常邮件,要么是垃圾邮件, 也就是说,p N + p S = 1. 同时,既然pSucc N f表示已知一封邮件是正常邮件时,它具有特征f的概率, 那么一封正常邮件具有特征f的概率就是(p N) * (pSucc N f). 同理,一封垃圾邮件具有特征f的概率就是(p S) * (pSucc S f). 因此一封邮件具有特征f的概率为:

haskell
p f = (p N) * (pSucc N f) + (p S) * (pSucc S f)

由此我们得到贝叶斯定理的一个变体,如果我们定义:

haskell
p -b = 1 - (p b)

那么贝叶斯定理就可以表述为:

haskell
pSucc a b =
    (p b) * (pSucc b a) /
    ((p b) * (pSucc b a) + (p -b) * (pSucc -b a))

垃圾邮件过滤系统不可能只检查一个词, 因此我们尝试推广到多个特征的情况:

haskell
pSucc [f1, f2 .. fn] S =
    (p S) * (pSucc S [f1, f2 .. fn]) / (p [f1, f2 .. fn])

其中,p Sp [f1, f2 .. fn]都很容易统计, 而pSucc S [f1, f2 .. fn]的计算复杂度很高, 特别是,S中同时具有[f1, f2 .. fn]的样本可能很少(稀疏), 那训练的效果就很差了。 那怎么办呢? 上一节我们假设结构具有马尔可夫性质,化简了问题, 这里我们也可以假设[f1, f2 .. fn]的每一项都是独立事件。 这样,pSucc S [f1, f2 .. fn]就可以化简为

haskell
(pSucc S f1) * (pSucc S f2) * .. * (pSucc S fn)

这样我们就只需要独立计算分类为S的情况下具有某一个特征的概率了, 避免了样本稀疏的问题, 同时,和上一节用马尔可夫结构化简一样,每一项都可以分布式地跑。

上面的式子中,如果某一项没有出现过, 也就是说,分类为S的情况下训练集的数据中不存在具有某特定特征的样本, 那一项的条件概率会为 0, 从而导致最后相乘的结果为 0, 也就是将其他各项的概率消除。 为了避免这个问题,我们可以强行将概率为 0 的项修正为一个小概率,比如 0.01, 具体数值无关紧要,因为以后如果训练集中新增了相应的样本,这个概率会自动得到修正的。 当然,这样有点粗暴。更合理的做法是所有的样本数都加 1, 并相应增加总数, 这样原本为 0 的样本数就变为 1, 避免了概率为 0 的情况。 因为训练集一般都很大,所以样本数加 1 没什么影响。 这种做法称为拉普拉斯平滑(Laplacian Smoothing). 当然,如果有必要,也可以改为加上一个小于 1 的正数。

和马尔可夫性质不一定成立一样,[f1, f2 .. fn]的每一项都是独立事件, 这个假设也不一定成立。 因此这个算法叫做幼稚贝叶斯分类器或者朴素贝叶斯分类器 (Naive Bayes classifier). 这个名称就是强调独立事件的假设不一定成立。

尽管独立事件的假设常常是不准确的,但幼稚贝叶斯在实际工程中出乎意料地好用。 因为很多应用并不在乎精确的类概率,只关心最后的分类结果。 比如垃圾邮件过滤,只需要判断是否是垃圾邮件, 并不需要在用户界面显示「本邮件有 87.53% 的概率是垃圾邮件」之类的信息。

贝叶斯网络和神经网络

然而,当特征之间相关性比较强,而我们又要求比较精确的类概率的时候,幼稚贝叶斯就不够用了。

也就是说,下面的式子里p [f1, f2 .. fn]不好算了。

haskell
pSucc [f1, f2 .. fn] S =
    (p S) * (pSucc S [f1, f2 .. fn]) / (p [f1, f2 .. fn])

回顾一下,如果[f1, f2 .. fn]是独立事件,那我们有:

haskell
p [f1, f2 .. fn] =
    (p f1) * (p f2) * .. * (p fn)

现在每一项特征的概率可能受其他特征的影响。 假设有一个函数pp可以表达这些影响

haskell
p [f1, f2 .. fn] =
    (pp f1) * (pp f2) * .. * (pp fn)

那么问题就在于pp是如何定义的? 既然pp表达的是某一事件受其他事件的影响,那pp就可以用条件概率来定义:

haskell
pp f = pSucc (influenced f) f

对于给定的特征f,我们找出所有影响它的特征[inf1, inf2 .. infn]

haskell
influenced f =
    pSucc [inf1, inf2 .. infn] f

pSucc [inf1, inf2 .. infn] f这看起来是不是很熟悉? 我们上面讨论幼稚贝叶斯的时候提到的是pSucc [f1, f2 .. fn] S. 这两者的结构是一样的。

因此,这其实是一个递归调用幼稚贝叶斯分类器的问题。

基于同样的思路,我们可以处理(pSucc S [f1, f2 .. fn]).

不过实际工程中,因为数据量很大,递归调用贝叶斯分类器是吃不消的。 所以为了降低计算复杂度,我们需要进行一些简化:

  1. 当影响程度很低时,我们直接忽略,视为独立事件。
  2. 类似马尔可夫性质,我们只考虑直接影响给定特征f的特征,不考虑间接影响。例如,pinf1可能通过影响inf1来间接影响f,这种情况不考虑。
  3. 当我们考虑影响给定特征f的特征时,假定这些影响f的特征是相互独立事件。

当然,如果有必要,上面的简化也可以放宽,以提高计算复杂度为代价,获得更准确的估计。

进行上述简化后,我们得到了贝叶斯网络(Bayesian network). 之所以称为贝叶斯网络,是因为特征间的相互影响关系,我们用有向无环图(DAG)来表示。 而特征对给定特征的具体影响程度,我们用条件概率表(CPT)来表示。 其中,特征间的相互影响关系,也就是DAG的构建,依赖于经验或领域知识。 条件概率表,则可以用幼稚贝叶斯来改进。

贝叶斯网络就介绍到这里。 现在我们换个思路,不从条件概率和贝叶斯定理入手,而从打分的角度来分类。 假设数据具有特征[f1, f2 .. fn],每个特征对应一个分值(权重), 我们把这些特征的分数加起来,得到一个总分,然后和一个目标分数(阈值)比较, 大于阈值结果为1,否则结果为0。

haskell
f [f1, f2 .. fn] =
    if (w1 * f1 + w2 * f2 + ... + wn * fn > t) then
        1
    else
        0

这个函数f叫做感知器(perceptron)。 感知器的定义,受到了神经元的启发。 感知器接受多个输入,返回一个布尔值, 就像神经末梢接受外部的输入,决定是否激动。

我们注意到,感知器主要的工作是计算一个多项式的值:

haskell
w1 * f1 + w2 * f2 + ... + wn * fn

那么从直觉上,线性不可分的问题,比如异或(XOR)函数,就无法转化成感知器的形式。

但实际上,感知器并没有这么弱,将感知器组合一下,就可以表达异或函数。

我们准备两个阈值为 0 的感知器,一个是x-y, 另一个是-x+y, 将输入分别发给这两个感知器:

输入 x-y -x+y 输出
(1, 0) 1 0 (1, 0)
(0, 1) 0 1 (0, 1)
(0, 0) 0 0 (0, 0)
(1, 1) 0 0 (0, 0)

然后再将输出提供给一个阈值为 0 的x+y感知器:

输入 中间结果 最终输出
(1, 0) (1, 0) 1
(0, 1) (0, 1) 1
(0, 0) (0, 0) 0
(1, 1) (0, 0) 0

比较输入和最终输出,可以看到我们的这三个感知器运算的结果是符合异或的定义的。

这里,前两个感知器(x-y-x+y)是第一层,最后一个感知器(x+y)是第二层。 由此我们看到,通过组合感知器,可以构成一个分层的神经网络, 分层的神经网络可以解决线性不可分问题。

但是感知器还是看起来很弱啊。 异或函数这么简单的问题,都要三个感知器,两层网络才能搞定。 而稍微正常一点的编程语言,异或函数都能很直接地定义。 我们何必要自废武功用感知器和神经网络呢?这不是自虐吗?

实际上,感知器和神经网络看起来很弱,但它也有优点:

  1. 感知器的「接口」很齐整,每个感知器都有多个输入,返回一个结果,这就使得它们组合起来很容易。

  2. 感知器内部都是在进行多项式运算,而不像普通函数一样千变万化,因此优化起来很容易(特别是现在我们有很强劲的 GPU)。

  3. 感知器的运算结果只取决于它的输入,因此可以很容易地以分布式的方式跑。

  4. 上面那个例子中x-y, -x+y, x+y的确定,来自于我们对异或函数的理解。假设我们对异或函数一无所知,感知器的结构决定了,我们比较容易通过暴力的方式来尝试各种权重和阈值。相反,我们不太可能通过暴力的方式生成字符串恰巧撞对异或函数的一般定义。

  5. 神经网络分层的结构,意味着我们可以逐层尝试,来逼近预期的结果。

看起来,神经网络还是有它的优势的。 不过还是觉得不怎么靠谱呀。 神经网络看起来像是在我们不知道怎么猜比较好的情况下的一种暴力的猜法。 这不是乱枪打鸟吗? 相反,贝叶斯网络也是猜,可是背后有概率论这样坚实的基础,出了错也比较容易找到原因, 而不像神经网络基本是一个黑盒子。

但是,别忘了贝叶斯网络特征间的联系,那个有向无环图的构建,是依赖领域知识的。 如果这个领域缺乏足够的研究,没有人能构建足够好的有向无环图, 或者这个领域只有极少数专家才了解,而那些专家没有时间或兴趣来构建那个有向无环图, 那贝叶斯网络就发挥不了它的威力。

相反,神经网络对领域知识的要求要低很多。 对领域知识的低要求,对神经网络来说,算是「一招鲜,吃遍天」了。 而随着计算力的提升,粗暴堆料的成本越来越低,神经网络也就越来越受重视。

当然,贝叶斯网络和神经网络并不是互斥的,有时两者可以结合使用。 当计算量非常大的时候,神经网络暴力尝试的效果可能不太好。 这时我们可以借助贝叶斯网络来更精准地调节神经网络的参数。

另外,以上只是神经网络的基本原理。实际使用的神经网络要复杂很多。

比如,我们的感知器只能输出 0 或者 1, 而既然是暴力尝试,那我们就希望整个网络对参数的调整敏感一点。 这时候我们就不再比较多项式的值和阈值来输出 0 或者 1, 而是将阈值转化成偏置加到多项式上, 并使用一个激活函数对多项式的结果进行处理, 得到一个浮点数。 最简单的激活函数是 ReLU, 定义很简单 max 0 n. 类似朴素贝叶斯,ReLu 虽然简单,但出奇地好用。

另外,实际使用的神经网络,无论是规模还是结构都非常复杂。

总结

强人工智能时代企图基于通用的逻辑表示一切人类的思考活动,这一尝试失败了。 人工智能的研究方向从通用的逻辑转移到专门领域的特定问题。 而即使专门领域的特定问题,也往往要借助机器的蛮力(统计)来猜。 从贝叶斯网络到神经网络,猜测的方法越来越暴力,也越来越依赖机器的蛮力。 而高度依赖机器的蛮力的神经网络,却成为一种解决各个领域机器学习问题的通用范式。 优雅的逻辑没能达到通用智能,而暴力的神经网络反倒在不断增长的算力的支持下朝着通用的方向前进。 也算是出人意料的展开呢。