本节书摘来异步社区《机器学习项目开发实战》一书中的第2章,第2.5节,作者:【美】Mathias Brandewinder(马蒂亚斯·布兰德温德尔),更多章节内容可以访问云栖社区“异步社区”公众号查看
实现通用算法之后,我们最终可以回到手上的问题——识别哪些消息是非垃圾短信,哪些是垃圾短信。Train函数的签名提供了目标的清晰概况:要获得分类器,需要一个示例训练集、一个标记化程序和选用的标记。我们已经得到了训练集,现在的目标是使用交叉验证指导分析,确定标记化程序和标记的最佳组合。
考虑到上述情况,我们先完成可行的最简单工作,首先是标记化。我们所要做的是取得一个字符串,将其分解为单词,忽略大小写。
这项工作需要正则表达式:w+模式匹配由一个或者多个“语言符号”(也就是字母或者数字,不包含标点符号)组成的“单词”。我们用该模式创建一个正则表达式matchWords,在script.fsx中按照经典的管道结构编写一个tokens函数,将输入字符串转换为小写,并应用正则表达式。matchWords.Matches创建应用表达式时找到的所有匹配项的集合MatchCollection。我们将该集合转换为Match序列,读出匹配值(每个匹配的字符串),最后将其转换为一个包含正则表达式识别出的所有单词的集合。在脚本文件中现有的用于读取数据集的代码之后,添加如下代码:
程序清单2-7 使用正则表达式标记化一行文本
open System.Text.RegularExpressions let matchWords = Regex(@"\w+") let tokens (text:string) = text.ToLowerInvariant() |> matchWords.Matches |> Seq.cast<Match> |> Seq.map (fun m -> m.Value) |> Set.ofSeq``` 这段代码看起来有些粗糙,说实话,如果不是因为.NET正则表达式奇怪的设计特征,它看上去应该更简单。无论如何,我们已经有了所需要的东西:将字符串分解为所包含单词的函数。 ####2.5.2 交互式验证设计 初看之下,程序清单2-7中的代码似乎可以完成我们所需的功能。但是“似乎可以”还不能令人满意,更好的做法是验证实际工作情况。用C#编码时,我的习惯是采用测试驱动开发(TDD):先填写一个测试,然后编写满足要求、通过测试的代码。TDD的好处之一是可以独立运行整个应用程序的小部分代码,快速确认它按照意图工作,然后逐步充实设计。在F#中,我倾向于采用稍有不同的工作流。F# Interactive可以实现更流畅的设计试验方式。你可以简单地在F# Interactive中试验,一旦设计成型,则提升代码进行单元测试,而不是立即提交设计并编写相关测试——那可能需要在以后重构。 在我们的例子中,检查token函数是否正常工作很简单。只需要在FSI中输入下面的语句并运行:tokens "42 is the Answer to the question";;`你将立刻在F# Interactive窗口中看到结果:
val it : Set = set ["42"; "answer"; "is"; "question"; "the"; "to"]我们的函数看起来工作得很好。所有单词已经分隔,包括数字“42”,重复的“the”在结果中只出现一次,“Answer”中的大写字母“A”也已经转换为小写。此时,如果我们编写的是用于生产环境的真实库,可以将token函数从探索性脚本中转移到解决方案中的相应文件,然后将这一个小代码片段转换为真正的单元测试。现在,我们“只是探索”,所以将保持原状,不马上编写测试。
有了一个模型,我们就要观察它的运行状况。我们将要使用的脚本和以前一样,所以已经加载了数据集中的所有短信——(DocType * string)数组。和前一章中一样,我们将使用交叉验证,数据集的一部分用于训练,剩下的保留,用于验证每个模型效果好坏。我随意保留1000个观测值用于验证,剩下的观测值都用于训练。为了确认所有代码正常工作,我们尝试一个相当粗糙的模型,只使用标记“txt”进行决策:
#load "NaiveBayes.fs" open NaiveBayes.Classifier let txtClassifier = train training wordTokenizer (["txt"] |> set) validation |> Seq.averageBy (fun (docType,sms) -> if docType = txtClassifier sms then 1.0 else 0.0) |> printfn "Based on 'txt', correctly classified: %.3f"``` 在我的机器上得出的正确率为87.7%。这已经很不错了!不是吗?如果没有背景信息,87.7%这样的数字似乎已经很令人满意,但是必须正确看待。如果我使用最蠢的模型,不管SMS的内容为何都始终预测“非垃圾邮件”,在验证集上的成功率也可以达到84.8%,因此87.7%的成绩没有什么了不起。 我们所要强调的重点是,应该始终从确立基准开始:如果使用可能的最简策略,预测的效果如何?对于分类问题,始终预测频率最高的标签(正如上面所做的)可以提供一个出发点。对于时间序列模型(根据过去的情况预测明天),预测明天的情况和今天一样是经典的做法。在一些情况下,20%的分类正确率可能已经很出色了,而在其他情况下(例如非垃圾短信和垃圾短信的例子),85%的正确率都令人失望。基准完全取决于数据。 第二个重点是,应该很小心地对待数据集的构建方式。在分割训练和验证集时,我简单地使用前1000个数据项作为验证集。想想看,我的数据集按照标签排序,所有垃圾短信示例在前,然后是所有非垃圾短信。这很糟糕:我们实际上在几乎只包含非垃圾短信的样本上训练分类器,只在垃圾短信上进行测试。你最起码可以预计到,评估质量将会相当差,因为分类器学习识别所用的数据与实际处理的数据大不相同。 对于所有模型,这都是个问题。然而,在我们的例子中因为简单贝叶斯分类器的工作方式,这个问题特别严重。回忆一下算法的工作原理,本质上,简单贝叶斯算法以两个因素确定权重:标记在不同组的相对频率,以及组本身的频率。质量不佳的采样不会影响第一部分,但是,每个组的频率可能完全脱离实际,这将大大影响预测。例如,训练集中非垃圾短信多于垃圾短信,将导致分类器完全低估消息是垃圾短信的可能性,偏向于预测非垃圾短信,除非有占据压倒性优势的证据。