2.1 挑战:构建一个垃圾邮件检测引擎
我们将要处理的文档不是电子邮件,而是文本消息。我们的目标是使用来自英国的真实SMS(短消息服务)数据集发现垃圾短信,这些消息是我们从加州大学欧文分校机器学习知识库中找到的。
■ 附注:
UCI机器学习知识库由加州大学欧文分校的机器学习与智能系统中心于1987年启动。你可以在http://archive.ics.uci.edu/ml/
中找到这个知识库,它包含将近300个干净、文档齐备的数据集,可以按照大小、特征类型等条件搜索和组织。这是一个很有趣的机器学习资源,包含了许多常常用作算法性能评估基准的“经典”数据集。
2.1.1 了解我们的数据集
在讨论使用哪个模型之前,首先来观察一下数据。数据集可以从http://1drv.ms/1uzMplL
下载(这就是UCI知识库中原始文件的复制品,原始文件可以在http://archive.ics.uci.edu/ml/ datasets/SMS+Spam+Collection
上找到)。它作为单个文本文件保存,名为SMSSpamCollection(没有文件扩展名),包含5574条真实的文本消息。每行是一个SMS,标记为“ham”(非垃圾短信)或者“spam”(垃圾短信)。下面是前面几行:
ham Go until jurong point, crazy.. Available only in bugis n great world la e buffet...Cine there got amore wat...
ham Ok lar... Joking wif u oni...
spam Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's
ham U dun say so early hor... U c already then say...
ham Nah I don't think he goes to usf, he lives around here though
spam FreeMsg Hey there darling it's been 3 week's now and no word back! I'd like some fun you up for it still? Tb ok! XxX std chgs to send, £1.50 to rcv```
第一眼的印象是,人们在文本消息中使用的语言明显不同于“标准英语”!例如“U c”这样的片段是“You see”的简写,在普通的词典中可能找不到。
考虑到这一点,我的第一步是尝试获得两组文本之间差别的“直觉”。有没有一些词语在垃圾短信或者非垃圾短信中出现得更频繁?这能够指导我们构建一个智能引擎,自动区分非垃圾短信和垃圾短信。我们首先按照类别(“ham”和“spam”)分解数据集,计算各自的词语出现频率。鉴于这一活动的探索特性,似乎很适合使用F#脚本。
■ 提示:
花点时间研究一下数据!盲目对数据集应用算法可能工作得不错,但是走不了太远。就像开发应用程序时应该花时间学习领域模型那样,对数据的更深入理解能够帮助你构建更智能的预测模型。
####2.1.2 使用可区分联合建立标签模型
数据是首要的,我们将采取和第1章相同的方式,但是有一些变化。数字识别数据集是由数字组成的(像素编码和标签都是如此),而这里的数据有一个特征(文本消息本身)和一个标签(“ham”或者“spam”)。我们应该如何表现它们?
可能的方法之一是将标签编码为布尔值,如非垃圾短信表示为“真”(true),垃圾短信表示为“假”(false)。这种标签工作得很完美,但是也有一些缺点:它不能自动说明字段的含义。例如,如果展示这样编码的一个数据记录:
True Ok lar... Joking wif u oni... ,`
你怎么可能猜出这里的“True”代表什么?另一个潜在的问题是,如果我们需要增加更多的类别(如非垃圾短信、垃圾短信和有歧义的信息),布尔值无法扩展。
机器学习的难点在于不能增加额外、不必要的复杂性。因此,我们将用F#的可区分联合(有时我也将它称作DU)表示标签。如果你之前从未见过可区分联合,可以近似地将其看作C#枚举类型。可区分联合和枚举一样,都定义一组独特的情况,但是功能强大得多。这种类比对DU不公平,但是足以帮助我们起步。
我们先使用可以在FSI中运行的例子,简单地说明DU的工作方式。定义DU和定义其他类型一样简单:
type DocType =
| Ham
| Spam```
在F# Interactive窗口运行上述语句(在最后添加;;触发求值),应该看到如下结果:
type DocType =
| Ham
| Spam```
上述语句定义了一个简单类型DocType,它只能取两个值:Spam或者Ham。我喜欢可区分联合的主要原因之一是它们和模式匹配配合得很好,可编写以非常清晰的风格描述业务领域的代码。例如,在我们的例子中,训练集中的每个示例都是非垃圾短信或者垃圾短信,并包含真实消息的内容。我们可以将其表示为一个元组,每个示例是一个DocType和一个字符串,按照这一路线处理消息示例,可以在F# Interactive窗口中尝试:
let identify (example:DocType*string) =
let docType,content = example
match docType with
| Ham -> printfn "'%s' is ham" content
| Spam -> printfn "'%s' is spam" content```
这个例子只是为了示意目的,但是指出了以后将要遵循的模式。在这个例子中,我们创建了一个小函数,取得一个消息示例并打印其内容以及所属类别。我们首先通过元组模式匹配将消息示例分为两部分(DocType和内容),然后在DocType的两种可能情况上使用模式匹配,在单独“分支”中处理两种情况。在FSI中输入:
identify (Ham,"good message");;`
你应该看到如下结果:
'good message' is ham```
在能够更好地决定如何处理真实SMS内容之前,这就是我们的数据模型。下面从加载数据集入手!
####2.1.3 读取数据集
和第1章一样,我们将把大部分时间花在探究数据集,从脚本环境中积极构建和改进模型上。打开Visual Studio创建一个新的F#库项目,将其命名为HamOrSpam。为了方便起见,我们还从Visual Studio之外使用文件系统,在解决方案中添加一个名为Data的文件夹,将数据文件SMSSpamCollection拖入其中(从前面提到的链接中下载,没有文件扩展名)。参见图2-1。
<div style="text-align: center"><img src="https://yqfile.alicdn.com/3d808bd20e5c9608fd298713670641641f250c8d.png" width="" height="">
</div>
这就使我们可以使用一个小技巧,用比第1章更清晰的方式访问数据文件。F#有两个方便的内建常量__SOURCE_DIRECTORY__和__SOURCE_FILE__,大大简化了脚本中文件的处理,我们可以引用数据文件位置,而无须硬编码一个取决于本地机器的路径。从这些常量的名称你可能已经猜到,第一个常量求得包含文件本身的目录完整路径,第二个返回到文件本身的完整路径,包括文件名。
现在可以进行脚本的编写了。和数字识别器使用的数据集之间主要的差别是,这个数据集没有文件头,只有两列,不使用逗号而使用制表符分隔。其他方面大部分相同:我们有一个包含示例的文件,希望将其提取到一个数组中。果不其然,解决方案看起来非常相似:读取文件并向每一行应用一个函数,将其解析为标签和内容,根据制表符“\t”拆分。
我们直接进入项目中的Script.fsx文件,删除默认的代码并粘贴如下代码:
程序清单2-1 从文件中读取SMS数据集
open System.IO
type DocType =
| Ham
| Spam
let parseDocType (label:string) =
match label with
| "ham" -> Ham
| "spam" -> Spam
| _ -> failwith "Unknown label"
let parseLine (line:string) =
let split = line.Split('\t')
let label = split.[0] |> parseDocType
let message = split.[1]
(label, message)
let fileName = "SMSSpamCollection"
let path = SOURCE_DIRECTORY + @"....Data" + fileName
let dataset =
File.ReadAllLines path
|> Array.map parseLine```
此时,你应该可以选择脚本中刚刚编写的所有代码,在F# Interactive窗口中执行,产生如下结果:
val dataset : (DocType * string) [] =
[|(Ham,
"Go until jurong point, crazy.. Available only in bugis n grea"+[50 chars]);
(Ham, "Ok lar... Joking wif u oni...");
(Spam,
"Free entry in 2 a wkly comp to win FA Cup final tkts 21st May"+[94 chars]);
// Snipped for brevity
(Ham,
"Hi. Wk been ok - on hols now! Yes on for a bit of a run. Forg"+[117 chars]);
(Ham, "I see a cup of coffee animation"); ...|]```
现在我们有了一个示例数据集,每个都标记为垃圾短信或者非垃圾短信。我们可以开始解决真正感兴趣的问题了:可使用哪些特征区分垃圾短信和非垃圾短信?
■ 提示: