《Arduino家居安全系统构建实战》——2.6 改进分类器

简介:

本节书摘来异步社区《机器学习项目开发实战》一书中的第2章,第2.6节,作者:【美】Mathias Brandewinder(马蒂亚斯·布兰德温德尔),更多章节内容可以访问云栖社区“异步社区”公众号查看

2.6 改进分类器

现在我们已经有了基准—— 低于84.4%是糟糕的结果,至少要超过87.7%。是时候观察使用可以自由支配的两大手段(标记化程序和分类器选用的标记)能达到什么效果了。

2.6.1 使用每个单词

利用一个单词,我们就能将分类器的正确率从84.8%提升到87.7%。如果使用每个可用的标记代替单个词汇,预测正确率当然应该大幅提高。我们来尝试一下这个思路,首先,必须从训练集中提取每个标记。我们编写一个vocabulary函数,对每个文档应用标记化程序,将标记合并为单一集合,实现上述功能。然后,从训练集中读取标记——用snd取出每个文档元组中的第二个元素,通过管道进入vocabulary函数:

let vocabulary (tokenizer:Tokenizer) (corpus:string seq) =
    corpus
    |> Seq.map tokenizer
    |> Set.unionMany

let allTokens =
    training
    |> Seq.map snd
    |> vocabulary wordTokenizer```
干脆利索。现在我们可以使用大的标记列表训练新的分类器,评估其性能:

let fullClassifier = train training wordTokenizer allTokens

validation
|> Seq.averageBy (fun (docType,sms) ->

if docType = fullClassifier sms then 1.0 else 0.0)

|> printfn "Based on all tokens, correctly classified: %.3f"`
结果相当令人失望,没有出现我们希望的重大改善:“根据所有标记,正确分类的比例为0.864”。我们必须面对这一失败,得到的模型仅仅胜过最简单的预测程序,比依赖单一词汇“txt”决策的前一个模型更差。

在此得到的教训是,盲目地向一个标准算法中投入许多数据,只会离目标越来越远。在我们的例子中,精心选择了一个特征,形成的模型更简单、更快、更擅长于预测。选择合适的特征是构建好的预测程序的关键部分之一,也是我们将要尝试推进的。在本例中,可以在两方面改变特征的定义,而无须更改算法本身:可以使用不同的标记化程序,以不同方式利用消息,也可以选择(或者忽略)不同的标记集合。

忙于这些工作时,我们将会多次重复某些代码,可以将整个“训练——评估”过程打包为一个函数,该函数以一个标记化器和一组标记为参数,运行训练过程,打印输出评估结果:

程序清单2-8 通用模型评估函数

let evaluate (tokenizer:Tokenizer) (tokens:Token Set) =
    let classifier = train training tokenizer tokens
    validation
    |> Seq.averageBy (fun (docType,sms) ->
        if docType = classifier sms then 1.0 else 0.0)
    |> printfn "Correctly classified: %.3f"```
评估特定模型变成一行代码:

evaluate wordTokenizer allTokens;;
Correctly classified: 0.864`

这为我们提供了相对简单的推进过程:选择一个标记化器和一组标记,调用evaluate函数观察特定组合是否好于之前尝试的组合。

2.6.2 大小写是否重要?

我们从标记化程序开始,回忆之前的讨论,目前使用的wordTokenizer函数忽略大小写,从它的角度,“txt”和“TXT”之间没有差别——两者被视为同一个标记。

这并不是不合理的做法。例如,考虑消息“Txt me now”和“txt me now”(给我发文本消息),忽略大小写,我们实质上将这两条消息视为意义上没有任何差别的消息。大小写没有带来任何相关的信息,可以当成噪声。

相反,考虑这两条消息:“are you free now?”(你现在有空吗?)和“FREE STUFF TXT NOW”(这是有关免费物品的短信)——这时,忽略大小写就可能丢失信息。

那么,哪一种方法才对呢?我们来尝试一下,看看不将所有字符串转换为小写的不同标记化程序是否比wordTokenizer更好:

程序清单2-9 使用正则表达式标记化一行文本

let casedTokenizer (text:string) =
    text
    |> matchWords.Matches
    |> Seq.cast<Match>
    |> Seq.map (fun m -> m.Value)
    |> Set.ofSeq

let casedTokens =
    training
    |> Seq.map snd
    |> vocabulary casedTokenizer

evaluate casedTokenizer casedTokens```
在FSI中运行达到87.5%的正确率。一方面,我们的结果仍然低于依赖“txt”的简单分类器;另一方面,两者已经相当接近(87.5%对 87.7%),这明显好于使用所有单一标记的wordTokenizer(86.4%)。

####2.6.3 简单就是美
我们得到了表面上更好的标记化程序,但是,添加大量特征所得到的结果仍然是效果更差、更缓慢的分类器。这明显不理想,也是反直觉的:增加更多信息怎么可能使模型更差?

让我们从另一方面去看待问题,思考之前的单标记模型为什么工作得更好。我们选择“txt”作为标记的原因是它在垃圾短信中经常发现,而在非垃圾短信中很少出现。换言之,它很好地区分两组,相当特定于垃圾短信。

我们现在做的是使用每个单一标记,不管它们的信息量如何。结果是,我们在模型中可能引入了相当多的噪声。更有选择性的方法——可能只选择每个文档组中最常见的标记,会得到什么样的结果呢?

让我们从一个简单的函数开始,给定一组原始文档(简单字符串)和一个标记化程序,将返回最常用的标记:

let top n (tokenizer:Tokenizer) (docs:string []) =

let tokenized = docs |> Array.map tokenizer
let tokens = tokenized |> Set.unionMany
tokens
|> Seq.sortBy (fun t -> - countIn tokenized t)
|> Seq.take n
|> Set.ofSeq```

现在,我们可以将训练样本分拆为非垃圾短信(Ham)和垃圾短信(Spam),在此过程中去掉标签:

let ham,spam =
    let rawHam,rawSpam =
        training
        |> Array.partition (fun (lbl,_) -> lbl=Ham)
    rawHam |> Array.map snd,
    rawSpam |> Array.map snd```
我们提取和计数每个组的标签,提取前10%,用Set.union将其合并为一个标记集合:

let hamCount = ham |> vocabulary casedTokenizer |> Set.count
let spamCount = spam |> vocabulary casedTokenizer |> Set.count

let topHam = ham |> top (hamCount / 10) casedTokenizer
let topSpam = spam |> top (spamCount / 10) casedTokenizer

let topTokens = Set.union topHam topSpam`
现在,可以将这些标记整合为一个新的模型,评估其表现:

> evaluate casedTokenizer topTokens;;
Correctly classified: 0.952```
正确率从87.5%一跃达到95.2%!这是可观的改善,实现这一飞跃不是通过增加更多的数据,而是通过删除特征。

####2.6.4 仔细选择单词
我们还能做得更好吗?试试看!通过选择标记的一个子集,只保留携带有意义信息的标记,我们得到了很明显的改善。按照这一思路,可以开始检查非垃圾短信和垃圾短信中最常用的标记,这相当容易实现:

ham |> top 20 casedTokenizer |> Seq.iter (printfn "%s")
spam |> top 20 casedTokenizer |> Seq.iter (printfn "%s")`
运行上述语句产生表2-1中的结果:

a46cf1235fd447bd1d458e2d3760b8c050785ad4

我们立刻注意到两件事。首先,两个列表都很相似,在20个标记中,有8个是共同的(a、and、for、is、on、the、to、you)。而且,这些单词都非常通用,不管什么主题,任何语句都必然包含几个冠词和代词。这一列表并不重要,表中的标记可以描述为“填充材料”,它们没有告诉我们很多关于消息是非垃圾短信还是垃圾短信的情况,甚至在我们的分析中引入了一些噪声。从性能的角度看,这意味着我们花费了许多CPU周期分析传达有限甚至毫不相关信息的标记。因此,从分析中去掉这些标记可能是有益的。

在此之前,我想到了列表中出现的另一个有趣特征。和非垃圾短信不同,垃圾短信的列表中包含几个相当特殊的单词——如“call”“free”“mobile”和“now”,不包含“I”或者“me”。如果你考虑到这一点,那是很有意义的:常规的文本消息有许多种目的,而垃圾短信通常试图“钓鱼”,诱惑你做某些事情。在这样的背景下,看到“now”(现在)和“free”(免费)这些带着广告腔调的单词也就不足为奇了,因为通过奉承的方法达到目的通常没有错,垃圾短信很少和“I”(我)相关而更多地与“You”(你)相关也是正常的。

■ 注意:

什么是“钓鱼”?钓鱼是使用电子邮件、SMS或者电话从某人那里窃取财物的行为。钓鱼通常采用一定的社会工程(伪装成受尊重的机构、威胁或者引诱)使目标采取骗子无法独自完成的措施,比如单击一个在机器上安装恶意软件的链接。
我们将很快回到上述要点。与此同时,观察一下,能否删除“填充材料”。常见的方法是依赖这些单词的预定义列表,这通常称作“停用词”。在http://norm.al/2009/04/14/list-of- english-stop-words/上可以找到一个此类列表。遗憾的是,没有通用的正确方法,显然,这取决于编写文档所用的语言,甚至语境也很重要。例如,在我们的例子中,文本消息中常用的“you”缩写词“u”是一个很合适的“停用词”。但是,大部分标准列表不会包含它,因为这种用法与SMS的关联度极大,不太可能在其他文档类型中找到。

我们不依赖于停用词列表,而是采用更简单的方法。我们的topHam和topSpam集合中包含非垃圾短信和垃圾短信中最常用的标记。如果某个标记在两个列表中都出现,它很有可能就是在英语文本消息中常见的单词,不特定于非垃圾短信和垃圾短信。我们找出所有共有的标记(这对应于两个列表的交集),将它们从标记选择中删除,再次运行分析:

let commonTokens = Set.intersect topHam topSpam
let specificTokens = Set.difference topTokens commonTokens
evaluate casedTokenizer specificTokens```
利用这一简单的更改,分类SMS消息的正确率从95.2%上升到了97.9%。这可以认为是相当好的结果了,离100%越近,改善越难实现。

####2.6.5 创建新特征
在尝试提出新想法时,我常常觉得有用的方法之一是颠覆问题,以便从不同角度观察。举个例子,当我编写代码时,通常从想象“快乐路径”——程序完成某项功能所需的最少步骤——开始,然后实现它们。在测试代码之前,这是很棒的方法,你的心思已经完全集中在成功路径上,而难以考虑失败的情况。所以,我喜欢在测试时将问题颠倒并提问:“使这个程序崩溃的最快方法是什么?”我发现这一招非常有效,而且使测试变得很有趣!

让我们来试试用这个方法改进分类器。对每个类别中最常见单词进行观察最终可以得到很明显的改善,因为贝叶斯分类器依赖于分类中的常见词以识别类别。观察一下每个文档类别中最少见的词如何?稍微修改一下前面编写的代码,就可以提取分组中最少使用的标记:

程序清单2-10 提取最少使用的标记

let rareTokens n (tokenizer:Tokenizer) (docs:string []) =

let tokenized = docs |> Array.map tokenizer
let tokens = tokenized |> Set.unionMany
tokens
|> Seq.sortBy (fun t -> countIn tokenized t)
|> Seq.take n
|> Set.ofSeq

let rareHam = ham |> rareTokens 50 casedTokenizer |> Seq.iter (printfn "%s")
let rareSpam = spam |> rareTokens 50 casedTokenizer |> Seq.iter (printfn "%s")`

89c6204a541216371ab5c74e02e882e901174825

你是否注意到某种模式?垃圾短信列表中充满了电话号码或者文本编码。同样,这也很有意义:如果我成为“钓鱼”的目标,有人希望我做某件事情——在电话上,这意味着拨打某个号码或者用短信发送一个数字。

这也强调了一个问题:作为人类,我们立刻发现这个列表是“许多电话号码”,但是每个号码在模型中都被视为单独的标记,这些标记出现的很少。然而,SMS消息中存在电话号码似乎是垃圾短信的可能标记。我们可以创建一个新特征,捕捉消息是否包含电话号码,而不管这些号码是什么,解决这个问题。这将使我们能够计算电话号码在垃圾短信中出现的频率,并(潜在地)在我们的简单贝叶斯分类器中将其作为一个标记使用。

如果仔细研究少见标记列表,就有可能发现更特殊的模式。首先,列出的电话号码都有类似的结构:它们以07、08或者09开头,然后是9个其他号码。其次,列表中有其他一些数字,主要是5位数字,这很可能是短信编码。

我们为每种情况创建一个特征。每当看到07之后的9个数字,就将其转换为标记__PHONE__,每当遇到5位数字,就将其转换为__TXT__:

程序清单2-11 识别电话号码

let phoneWords = Regex(@"0[7-9]\d{9}")
let phone (text:string) =
    match (phoneWords.IsMatch text) with
    | true -> "__PHONE__"
    | false -> text

let txtCode = Regex(@"\b\d{5}\b")
let txt (text:string) =
    match (txtCode.IsMatch text) with
    | true -> "__TXT__"
    | false -> text

let smartTokenizer = casedTokenizer >> Set.map phone >> Set.map txt```
smartTokenizer简单地将3个函数链接在一起。casedTokenizer函数取得一个字符串并返回一个字符串集合,包含单独识别的标记。因为它返回的是字符串集合,可以应用Set.map,对每个标记运行phone函数,将看起来像电话号码的标记转换为“__PHONE__”,然后同样执行txt函数。

在F# Interactive中运行如下代码,确认上述函数正常工作:

smartTokenizer "hello World, call 08123456789 or txt 12345";;

val it : Set =
set ["World"; "__PHONE__"; "__TXT__"; "call"; "hello"; "or"; "txt"]`
结果正如我们预期的那样——一切正常。在列表中人工添加刚刚创建的两个标记,尝试调整过后的标记化程序(如果不这么做,标记列表中仍然包含单独的数字,没有与“智能标记化程序”产生的__PHONE__标记匹配):

let smartTokens =
    specificTokens
    |> Set.add "__TXT__"
    |> Set.add "__PHONE__"

evaluate smartTokenizer smartTokens```
请击鼓喝彩……准确率又一次跃升,从96.7%上升到98.3%!考虑到目前的性能水平,这是非常了不起的提升,值得花一些时间讨论。我们转换了数据集,通过聚合现有特征创建了一个新特征。这一过程是开发好的机器学习模型的关键部分。从原始数据集(可能包含低质量数据)开始,随着对领域的理解的进一步深入,找出变量之间的关系,就可以不断将数据重塑为新的表现形式,更好地适应手上的问题。这种试验、将原始数据精炼为好的特征的循环是机器学习的核心。

####2.6.6 处理数字值
让我们以一个简短的讨论结束关于特征提取的部分。在本章中,我们只考虑离散特征,也就是说,只取一组离散值的特征。对于连续特征(或者数值特征)该怎么办?例如,想象一下,如果你的直觉是消息的长度很重要,能否使用到目前为止已经有的想法?

可能性之一是将问题简化为已知的问题,将消息长度(0~140个字符)变成一个二分特征,将其分为两类——短消息和长消息。这样,问题就变成了“区分长消息和短消息的合适长度值是多少?”

我们可以直接用贝叶斯定理解决这个问题:对于指定的阈值,我们可以计算长于该阈值的消息是垃圾短信的概率。然后,识别“好”的阈值就只是测试不同数值,选择信息量最大的一个。下面的代码完成上述功能,应该不太难以理解:

程序清单2-12 根据短信长度计算是垃圾短信的概率

let lengthAnalysis len =

let long (msg:string) = msg.Length > len

let ham,spam =
    dataset
    |> Array.partition (fun (docType,_) -> docType = Ham)
let spamAndLongCount =
    spam
    |> Array.filter (fun (_,sms) -> long sms)
    |> Array.length

let longCount =

    dataset
    |> Array.filter (fun (_,sms) -> long sms)
    |> Array.length

let pSpam = (float spam.Length) / (float dataset.Length)
let pLongIfSpam =

float spamAndLongCount / float spam.Length

let pLong =

float longCount /
float (dataset.Length)

let pSpamIfLong = pLongIfSpam * pSpam / pLong

pSpamIfLong

for l in 10 .. 10 .. 130 do

printfn "P(Spam if Length > %i) = %.4f" l (lengthAnalysis l)```

运行上述代码,将会看到一条短信是垃圾短信的概率随着长度的增长而显著增大,这并不奇怪。垃圾短信制造者发送许多消息,希望得到最合理的收益,因此会在单条SMS中加入尽可能多的内容。现在,我将暂时搁置这一话题,在第6章中从稍有不同的角度讨论,但是我希望说明的是,贝叶斯定理在许多不同的情况下都很方便,不需要花费太多的精力。

相关文章
|
22天前
|
网络协议 安全 Linux
linux系统安全及应用——端口扫描
linux系统安全及应用——端口扫描
35 0
|
1月前
|
监控 安全 Linux
Linux系统的防御从多个方面来保护系统安全
防火墙:使用防火墙软件如iptables或Firewalld来限制网络流量,保护系统免受恶意网络攻击。
|
11月前
|
安全 Linux
Linux 系统安全 - 近期发现的 polkit pkexec 本地提权漏洞(CVE-2021-4034)修复方案
Linux 系统安全 - 近期发现的 polkit pkexec 本地提权漏洞(CVE-2021-4034)修复方案
1018 1
|
安全 网络协议 Unix
Linux系统安全与应用
系统安全问题一直存在着,当系统往往出现安全漏洞的时候会对我们的系统运行有一定程度的影响,严重的话还会造成系统瘫痪等问题。
Linux系统安全与应用
|
安全 网络协议 算法
Linux系统安全及应用
⭐本文介绍⭐ 作为一种开放源代码的操作系统,Linux服务器以其安全,高效和稳定的显著优势而得以广泛应用。本文主要从账号安全控制、系统引导和登录控制的角度,介绍Linux系统安全优化的点点滴滴;还将介绍基于Linux环境的弱口令检测、网络扫描等安全工具的构建和使用,帮助管理员查找安全隐患,及时采取有针对性的防护措施。
Linux系统安全及应用
|
安全 关系型数据库 MySQL
Linux系统安全与应用(Centos7)(sudo)
Linux系统安全与应用(Centos7)(sudo)
315 0
|
缓存 网络协议 安全
Linux 系统安全及应用(账号安全和引导登录控制)(4)
1 账号安全基本措施 1.1 系统账号清理 1.1.1 将非登录用户的Shell设为/sbin/nologin 在我们使用Linux系统时,除了用户创建的账号之外,还会产生系统或程序安装过程中产生的许多其他账号,除了超级用户root外,其他账号都是用来维护系统运作的,一般不允许登录,常见的非登录用户有bin、adm、mail、lp、nobody、ftp等。 查看/etc/passwd 文件,可以看到多个程序用户。
153 0
|
存储 安全 算法
Linux 系统安全及应用(账号安全和引导登录控制)(3)
1 账号安全基本措施 1.1 系统账号清理 1.1.1 将非登录用户的Shell设为/sbin/nologin 在我们使用Linux系统时,除了用户创建的账号之外,还会产生系统或程序安装过程中产生的许多其他账号,除了超级用户root外,其他账号都是用来维护系统运作的,一般不允许登录,常见的非登录用户有bin、adm、mail、lp、nobody、ftp等。 查看/etc/passwd 文件,可以看到多个程序用户。
234 0
|
安全 Ubuntu Unix
Linux 系统安全及应用(账号安全和引导登录控制)(2)
1 账号安全基本措施 1.1 系统账号清理 1.1.1 将非登录用户的Shell设为/sbin/nologin 在我们使用Linux系统时,除了用户创建的账号之外,还会产生系统或程序安装过程中产生的许多其他账号,除了超级用户root外,其他账号都是用来维护系统运作的,一般不允许登录,常见的非登录用户有bin、adm、mail、lp、nobody、ftp等。 查看/etc/passwd 文件,可以看到多个程序用户。
171 0

热门文章

最新文章