“出大事了,出大事了,能不能帮帮我!”设计师一溜小跑来到了程序员桌前。
“我上个月开始,打算追一个女生,坚持每天给她写一封邮件,发送一点小小的问候。可是这一个月过去了,她一封也没有回过我……我以为只是女神懒得回邮件,但是今天鼓起勇气准备向她表白的时候,结果她告诉我从来不知道我在追她,也从来没有收到过我的情书邮件!”
程序员想都没想:“八成是被判成垃圾邮件放进垃圾箱了吧!邮件长什么样?”
设计师只好羞涩地打开其中一封:
(*^▽^*),我以后就叫你(*^▽^*),怎么样?
你总是有多面的情感,时而o(╥﹏╥)o,时而Σ(⊙▽⊙”a,突然≧◇≦,下一秒又╮( ̄▽  ̄)╭,但是每当我想起你的时候,还是希望你是(*^▽^*)的样子,不然我就会不(*^▽^*),你╮( ̄▽  ̄)╭,我就知道你可能>_<了;你≧◇≦,我就知道你可能会T_T,当你Σ(⊙▽⊙”a,你可能需要>3<,当你o(╥﹏╥)o,我会来:-*。当然,我的(*^▽^*),我希望你永远(*^▽^*)。
你的(*^▽^*)
程序员不禁扶额:“你这写的什么火星文啊……这些乱码一样的东西,能不被判成垃圾邮件才怪呢!你快坐好,我今天倒是跟你好好科普一下,你的邮件为啥变成了垃圾邮件。”
识别垃圾邮件,规则怎么写?
垃圾邮件,英语里是午餐肉(Spam)——就像那些我们并不关心、喜欢,也不需要,甚至有的时候还是虚假的内容,却源源不断地堆进我们的邮箱,跟午餐肉一样腻味。(这大概是午餐肉被黑得最惨的一次吧!)
那既然这些内容是用技术批量生产的,我们一定有方法,用批量的方式识别和删除它。搞清楚定义,再变成可执行的流程,并交给计算机执行就好。那么,具体应该怎么做?
最直接的方式,是写出可执行的定义,而且这个定义是明确地可用算法来描述,进而用计算机执行的。
比如,那些说你中奖,有一些色情引导或乱码,还很短的邮件就是垃圾邮件啦。但是这条不能被计算机所执行,具体到一封真实的邮件,可能会有模棱两可的结论,比如中一块钱算不算中奖,色情具体包括哪些,到底什么算乱码,等等。
算法必须是明确、完整、有限的步骤描述,这种描述性的语言人类读起来比较舒服,但是会让计算机和实现这个功能的程序员抓狂的。
那这规则该怎样写才是真正的算法呢?
我们可以设置一个算法式的明确简单的规则:包含“中奖”,“恭喜”,“服务”中任何一个词的邮件就是垃圾邮件。终于可以执行了,但是,直接执行也许会有误判,也许一条普通邮件也有“中奖”这个词,也许一条垃圾邮件并不包含上面的这些词。
这里似乎陷入一种令人抓狂的矛盾,要明确可执行,总感觉写出来的规则不完全对,那种“人话”的描述我们觉得没啥问题,但是并无法用计算机批量执行。
这里根本原因在于,“什么是垃圾邮件”这事,压根就没有大家都公认的答案。
好在,程序员们想出了如下解决思路:把人类群体对众多邮件是否是“垃圾邮件”的判别结论全部记录下来,那么在这个库中的每一条邮件,我们能给它戴上一个 “垃圾邮件率”来表示是否垃圾的程度。
比如,有10个人看过这个邮件,2个人认为垃圾,那就是0.2的垃圾邮件率了。如果垃圾邮件率超过一定值,那么我们就可以判定它是垃圾邮件了。
接下来,我们就把这个思路用技术语言翻译一下吧。
“垃圾邮件率”怎么算?
我们肯定没有条件把每一封邮件都给很多人看然后算比例,所以我们要另辟蹊径,通过一些已经标注的数据,用算法找到规律,进而自动化这一过程,估计出“垃圾邮件率”。
首先,我们需要建立一个库,里面尽可能包含各种各样的邮件以及其相关信息,例如地域、发送人、接收人,文本内容等等,并不断地、实时地更新,好让机器来“学习”它。这里收集的数据就是进行机器学习的第一步。
我们要让机器生成一个对于垃圾邮件的认识,并形成一个模型,可以输入一个邮件让机器通过模型来判定。我们假设,一定先有一些垃圾发送器和正常的发邮件的人(C = 0, 1,1为垃圾邮件),以P为比例分布。
我们来看一封邮件:“你好,请把钱打到我的卡上。”
首先,我们要告诉机器,所有的邮件当中大概有多少是垃圾邮件。比如:我们的库里有1000封邮件,其中只有10封是我们标记为垃圾的,那任何一封邮件为垃圾邮件的概率为1%。
P(C = 1)= 0.01
P(C = 0)= 0.99
这个在统计学上叫先验概率(prior probability),就是我们在进行具体的观察以前的以往经验的总结。
但是有先验概率是远远不够的,好在我们还有邮件本身的内容作为特征来使用。机器发现,“钱”这个词在垃圾邮件中有2封出现过,而在普通邮件中只有1次,那么有:
P(钱 = 1 | C = 1) = 2 / 10 = 0.2
P(钱 = 1 | C = 0) = 1 / 990 = 0.001
于是,邮件中是否出现钱字,就成了判断是否是垃圾邮件的重要特征了,和前面的先验概率合起来,得到的结果叫做后验概率:
P(C = 1) * P(钱 = 1 | C = 1) = 0.01 * 0.2 = 0.002
P(C = 0) * P(钱 = 1 | C = 0) = 0.99 * 0.001 = 0.00099
这两个数都好小,为了方便我们理解这具体的大小差距,我们把这两个结果除以他们的和,使得他们的结果和为1,得到符合“概率”定义的结果:
P(C = 1 | 钱 = 1) = 0.002 / ( 0.002 + 0.00099 ) = 0.67
P(C = 0 | 钱 = 1) = 0.00099 / (0.002 + 0.00099 ) =0.33
所以,这封邮件是垃圾邮件的概率是0.67,是我们计算出来的来估计人们认为这是垃圾邮件的比例。如果,我们假设超过0.5就可以判别为是垃圾邮件,那么这封邮件就会自然就被丢进了垃圾箱。
这种先验+后验的判定方法,就是大名鼎鼎的贝叶斯(Bayesian)公式了。
当然,这里以“钱”字出现与否作为了生成特征,来判断其可能性的大小,可能还有很多其他方面的内容,比如出现次数,总词数等等,我们一般假设这些特性是相互独立的,可以乘在一起来计算,最后得出一个综合的、一封邮件是不是为垃圾邮件的概率。
当然,这个垃圾邮件率的估计肯定和真实有一定偏差,来自于贝叶斯假设的局限性,独立假设不完全对,还有特征选择不够全面性等等。算法工程师的工作,就是不断地在一个应用场景里,去调整可能有问题的每一个细节,进而算得更准一些。
机器是怎样学习的?
其实,我们人在判断“垃圾邮件”的时候,往往并没有像上述生成模型一样,去假设完整过程。而更多可能是这样的:算个啥贝叶斯,直接用眼睛扫过,发现一些特征字眼,再合成高维抽象的特征,进而快速判断。
不过,人和机器有区别,真实的人的思维过程不可观测、也过于复杂,没法复现,而机器却天然有进行大量重复计算的能力。因此,我们可以借鉴人的思维方式,但也必须要理解机器的思维方式。
机器要做的事情,主要有两步:
第一步是特征抽取:特征即对象的某方面的特性,把真实对象映射到数值(01,分类,整数,实数等),能够从一个维度反映其性质。特征需要容易测量且对问题有价值,比如邮件的长度,是否含有“钱”这个字,“钱”这个字出现了多少次等就都是特征,这是计算机去认识一个现实对象,不那么完美,但是唯一的方式。
第二步是学习:特征构成了计算机输入信息的全部,那他们是怎么通过一系列计算变成最后判别的“垃圾邮件率”或“是否是垃圾邮件”的呢?我们以逻辑回归的公式举例子:
P = 1 / (1 + exp(- A’X + b))
这个式子可以输入一个向量X,然后必定得到一个0~1之间的数,恰好可以表示比例、概率之类的意思,但是不一定可靠。机器学习的任务便是找到最佳的A,b组合,使得它在已经观察到的样本上效果是最好的。
具体回到这个问题,我们可以抽取邮件的每个词或短语的词频、出现与否、邮件长度等作为特征,用一定的权重A把这些特征的值加起来,再带入公式,就可以得到最后的“垃圾邮件率”了。
其实人在学习中也有类似的过程,比如我们学习如何去辨别一个西瓜是否甜,我们从长辈那里学到,可以看看是否足够大,看看瓜藤是不是绿的,敲敲看声音是否清脆等等,最开始可能觉得够大就好了,茎活不活,声音清脆否无所谓,结果买了一个烂瓜回家。于是,下次则特别注重声音的清脆,买回家却发现肉虽然甜,但是皮很厚,不划算,一看藤也是死的。
于是,终于学会这三方面特征的取舍,声音差不多得了,个大也不重要,关键瓜藤要绿,终于形成了这三个特征的权衡,进而学会去判断一个西瓜是否甜了。
这里机器处理的两个步骤里,一是模拟人从客观对象上找特点的过程,当然计算机需要这个对象已经数字化记录在磁盘上。二是模拟人进行学习的过程。其实,哪有什么机器学习,正是我们自己的大脑构建了这套交给计算机工作的方法,从而解放自己的劳力。
总结思考
我们总算是把一条邮件是否是“垃圾邮件”这个问题解决了,来来来,划重点:
1. 这是个没有标准答案的问题,我们只能得到“垃圾邮件率”;
2. 这个问题我们写不出直接的规则来执行判断,只能通过对于样本的学习了;
3. 我们需要把这一判别过程记录下来,并日常更新。
我们借“垃圾邮件”识别,来浅显易懂地介绍了一把(文本)分类问题的解决思路,以及数学建模在计算机科学中的重要地位。数学模型是美好的,它能够记录本质而复杂的规律,并且在需要的时候提供可靠的信息支持。
程序员:“你听懂了吧?所以,你那些奇怪的表情符号正好和大多数垃圾邮件里用的乱码有一样的特征,自然就被误判咯,你们这搞艺术的果然还是不行啊!”
设计师:“好嘛,那下次写一封纸的放女神桌上!”
程序员:“人工智能都挑战不了还想挑战真智能?那就真的只有真·垃圾箱见了!”
(程序员卒)