这是一本用于学习基本数据挖掘知识的书籍。大部分关于数据挖掘的书籍都着重于讲解理论知识,难以理解,让人望而却步。不要误会,这些理论知识还是非常重要的。但如果你是一名程序员,想对数据挖掘做一些了解,一定会需要一本面向初学者的入门书籍。这就是撰写本书的初衷。
这本指南采用“边学边做”的方式编写,因此在阅读本书时,我强烈建议您动手实践每一章结束提供的练习题和实验题,使用书中的Python脚本将其运行起来。书中有一系列展示数据挖掘技术的实例,因此在阅读完本书后,你就能掌握这些技术了。这本书以Creative Commons协议发布,可以免费下载。你可以任意分发这本书的副本,或者重新组织它的内容。也许将来我会提供一本纸质的书籍,不过这里的在线版本永远是免费的。

注脚

展开查看详情

1. 目 录 致谢 阅前必读 简介 序 数据挖掘简介及如何使用本书 欢迎来到21世纪 本书的结构 推荐系统入门 你喜欢的东西我也喜欢 使用Python代码来表示数据 最后一个公式:余弦相似度 Python推荐模块 隐式评价和基于物品的过滤算法 显式评价&隐式评价 显式评价的问题 什么会阻碍你成功? 基于用户/物品的协同过滤 修正的余弦相似度 Slope One算法 使用Python实现Slope One算法 分类 根据物品特征进行分类 回到潘多拉 她是从事什么运动的? Python编码 每加仑燃油可以跑多少公里? 番外篇:关于标准化 进一步探索分类 效果评估算法和kNN 留一法 混淆矩阵 代码示例 Kappa指标 本文档使用 书栈(BookStack.CN) 构建 - 1 -

2. 优化近邻算法 新的数据集,新的挑战 朴素贝叶斯 朴素贝叶斯 微软购物车 贝叶斯法则 为什么我们需要贝叶斯法则? i100、i500健康手环 使用Python编写朴素贝叶斯分类器 共和党还是民主党 数值型数据 使用Python实现 朴素贝叶斯算法和非结构化文本 非结构化文本的分类算法 训练阶段 使用朴素贝叶斯进行分类 新闻组语料库 朴素贝叶斯与情感分析 聚类 层次聚类法 编写层次聚类算法 k-means聚类算法 安然事件 本文档使用 书栈(BookStack.CN) 构建 - 2 -

3.致谢 致谢 当前文档 《面向程序员的数据挖掘指南》 由 进击的皇虫 使用 书栈(BookStack.CN) 进行 构建,生成于 2018-03-27。 书栈(BookStack.CN) 仅提供文档编写、整理、归类等功能,以及对文档内容的生成和导出工 具。 文档内容由网友们编写和整理,书栈(BookStack.CN) 难以确认文档内容知识点是否错漏。如 果您在阅读文档获取知识的时候,发现文档内容有不恰当的地方,请向我们反馈,让我们共同携手, 将知识准确、高效且有效地传递给每一个人。 同时,如果您在日常生活、工作和学习中遇到有价值有营养的知识文档,欢迎分享到 书栈 (BookStack.CN) ,为知识的传承献上您的一份力量! 如果当前文档生成时间太久,请到 书栈(BookStack.CN) 获取最新的文档,以跟上知识更新换 代的步伐。 文档地址:http://www.bookstack.cn/books/DataminingGuideBook 书栈官网:http://www.bookstack.cn 书栈开源:https://github.com/TruthHun 分享,让知识传承更久远! 感谢知识的创造者,感谢知识的分享者,也感谢每一位阅读到此处的 读者,因为我们都将成为知识的传承者。 本文档使用 书栈(BookStack.CN) 构建 - 3 -

4.阅前必读 阅前必读 《面向程序员的数据挖掘指南》 欢迎辞 源码 GitBook 排版 《面向程序员的数据挖掘指南》 作者:Ron Zacharski CC BY-NC 3.0] https://github.com/egrcc/guidetodatamining 原文 http://guidetodatamining.com/ 译文来自 @egrcc 的 https://github.com/egrcc/guidetodatamining 根据译文做了排版优化,修正部分错误问题,支持语法高亮。 下载电子书: https://www.gitbook.com/book/yourtion/dataminingguide/details 直接下载:PDF、EPub、Mobi 欢迎辞 本文档使用 书栈(BookStack.CN) 构建 - 4 -

5.阅前必读 这是一本用于学习基本数据挖掘知识的书籍。大部分关于数据挖掘的书籍都着重于讲解理论知识,难 以理解,让人望而却步。不要误会,这些理论知识还是非常重要的。但如果你是一名程序员,想对数 据挖掘做一些了解,一定会需要一本面向初学者的入门书籍。这就是撰写本书的初衷。 这本指南采用“边学边做”的方式编写,因此在阅读本书时,我强烈建议您动手实践每一章结束提供的 练习题和实验题,使用书中的Python脚本将其运行起来。书中有一系列展示数据挖掘技术的实例,因 此在阅读完本书后,你就能掌握这些技术了。这本书以Creative Commons协议发布,可以免费下 载。你可以任意分发这本书的副本,或者重新组织它的内容。也许将来我会提供一本纸质的书籍,不 过这里的在线版本永远是免费的。 源码 DataminingGuideBook-Codes 本文档使用 书栈(BookStack.CN) 构建 - 5 -

6.阅前必读 GitBook 排版 Yourtion yourtion@gmail.com https://github.com/yourtion 如有修改建议优化,请直接Fork:https://github.com/yourtion/DataminingGuideBook/ 进行修改并申请 Pull Request。 本文档使用 书栈(BookStack.CN) 构建 - 6 -

7.简介 简介 第一章:简介 第一章:简介 原文链接:http://guidetodatamining.com/chapter1/ 本章将讲述什么是数据挖掘,它所能解决的问题的是什么,以及在阅读完本书后,你可以做些什么。 内容: 寻找事物 本书结构 阅读完本书后你可以做些什么? 为什么数据挖掘很重要?哪些内容可以为我所用? 标题里的“Numerati的古老艺术”是什么意思? 本文档使用 书栈(BookStack.CN) 构建 - 7 -

8.序 序 序 序 如果你每天都能重复做这些简单的事,你就会获得某种特别的力量。在你获得之前,这是特别的,但 获得之后,就没什么大不了的了。 ——鈴木 俊隆 在阅读本书之前,你可能会认为像潘多拉、亚马逊那样的推荐系统、或是恐怖分子用的数据挖掘系 统,一定会非常复杂,只有拥有博士学位的人才能够了解其中的算法。你也许会认为设计出这些系统 的人都是研究火箭技术的。而我撰写本书的目的之一就是希望能够揭开这些系统的神秘面纱,展示它 们所使用的基本原理。 虽然的确会有像Google工程师或是在国家安全局工作的天才技术人员,数据挖掘却是建立在一些基本 逻辑和方法之上的,非常易于理解。在阅读本书之前,你可能会认为数据挖掘是一种让人震惊的技 术,但阅读之后你会发现,其实也没什么大不了的。 上图中的日本文字“初心”,表示要始终保持一颗“初学者的心”,也就是一种开放的心态,接受各种可 能性。 下面这个故事你可能在哪儿听过(很有可能是来自李小龙的“龙争虎斗”):一位教授想要寻求指引, 于是来到一位智者面前,希望能得到点化。这个教授不停地说着自己毕生学到了什么,发表了多少论 本文档使用 书栈(BookStack.CN) 构建 - 8 -

9.序 文等等。这时,智者问他:“喝茶吗?”然后开始向教授的杯子里倒茶,一直倒,最后溢到了桌子上、 地上。“你在干什么?”教授大叫道。智者说:“我在倒茶。你的思想就像这个茶杯,已经倒满了茶, 容不下任何其他东西。你必须先放空你的思想,我们才能继续往下说。” 在我看来,优秀的程序员就像是空的茶杯,他不断地探索着新的技术(noSQL、node-js等等)。普 通的程序员沉浸在那些固有的想法中:C++很棒,Java不好,PHP只能用来编写网页,MySQL是数据 库的唯一选择。我希望你能够以开放的心态阅读本书,从而发现一些有价值的东西。正如铃木俊隆所 说: 在初学者眼中,世界充满了可能;专家眼中,世界大都已经既定。 本文档使用 书栈(BookStack.CN) 构建 - 9 -

10.数据挖掘简介及如何使用本书 数据挖掘简介及如何使用本书 数据挖掘简介及如何使用本书 数据挖掘简介及如何使用本书 想象我们身处一个150年前的美国小镇。大家都互相认识。 商店新进了一批布料,店员注意到几块印有特殊花纹的布料肯定会受到克兰西女士的喜爱,因为他知 道这位女士喜欢同类型的布料,并暗自记下如果克兰西女士下次到访,要将这块布料推荐给她。 温克勒周向酒吧老板威尔逊先生提到,他正考虑要将自己的雷明顿来福枪转售。威尔逊先生将这个消 息告诉了巴德巴克莱,他正想购买一把高品质的来福枪。 瓦尔克兹警官和他的下属们知道李派是个麻烦人物,因为他总是喝酒,脾气不好又身强力壮。100年 前的小镇生活全靠人与人之间的关系。 人们知道你的喜好,健康和婚姻状况。无论好坏,这都是一种个性化的体验。世界各自的社区都存在 这种高度个性化的生活状态。 让我们穿越100年,来到1960年代。个性化的交流会有所减少,但它依然存在。 一位常客走进书店时,店员会招呼道“米切纳的新书到了”,因为他知道这位常客喜欢米切纳的书。或 者他会推荐高华德的《The Conscience of a Conservative》,因为他知道这位常客是位坚定 的保守派。再如,来到餐厅的常客会被服务员小姐问道“照旧吗”。 即使在今天,也存在很多个性化的服务。我去附近的梅西亚咖啡店时,服务员会问“您是要一杯加量的 大杯拿铁吗”,因为她知道这是我每天早上都会喝的咖啡。 本文档使用 书栈(BookStack.CN) 构建 - 10 -

11.数据挖掘简介及如何使用本书 当我带着贵宾犬去宠物店修剪毛发时,美容师不需要问我要剪成什么造型,因为他知道我喜欢标准的 德国造型。 但时过境迁,如今的生活已和百年前的小镇不一样了。大型购物超市取代了邻家的小型商店或商贩, 这使得人们的选择变得有限起来。 福特曾说过:“顾客们可以让轿车喷上自己喜欢的颜色,不过前提是他喜欢的是黑色。” 音像店只会采购有限的音像制品,书店采购的书也是有限的。想要吃冰激凌?你可以选择香草味、巧 克力味,也许草莓味也有。想要买一台洗衣机?1950年的希尔士里只有两种机型:55美元的标准款和 95美元的豪华款。 本文档使用 书栈(BookStack.CN) 构建 - 11 -

12.欢迎来到21世纪 欢迎来到21世纪 欢迎来到21世纪 寻找相关产品 那要如何寻找商品呢? 不仅仅是寻找商品 数据挖掘扩展了我们的能力 海量数据挖掘不是星际争霸II才有的东西 欢迎来到21世纪 到了21世纪,选择范围有限的问题已经不复存在了。 想听音乐?iTunes里有1100万首曲目!截止到2011年10月,他们一共售出了160亿首歌曲。 还想要有更多选择?可以去Spotify,那里有超过1500万首歌曲。 想买一本书?亚马逊有200万本可供选择。 想看视频?那选择就更多了:Netflix(10万个视频)、Hulu(5万)、Amazon Prime(10万)。 想买一台笔记本电脑?在亚马逊可以搜索到3811条结果。 本文档使用 书栈(BookStack.CN) 构建 - 12 -

13.欢迎来到21世纪 搜索电饭煲则可以得到1000条结果。 相信在不久的将来会有更多的商品可供选择——上十亿的在线音乐,各种各样的视频节目,以及能够用 3D打印机定制的产品。 寻找相关产品 现在的问题在于——如何寻找相关的产品。 在那1100万首iTunes曲目中,肯定有一部分音乐是我特别喜爱的,我该如何找到它们? 我想在Netflix上观看一段视频,应该看什么呢?我想用P2P下载一部电影,哪部比较好呢? 而且问题会越来越严重——每分钟都有数以万记的媒体数据被发布到互联网上;共享群组里每分钟都会 新增100个文件;YouTube上每分钟都会有24个小时时长的新视频被上传;每小时会有180本新书发 布。每天都有新的东西可以购买,要想找到自己感兴趣的产品变得越来越难。 如果你是一位音乐人——比如马来西亚的季小薇——真正的威胁并不来自于你的专辑被他人非法下载,而 是大众根本找不到你的专辑。 本文档使用 书栈(BookStack.CN) 构建 - 13 -

14.欢迎来到21世纪 那要如何寻找商品呢? 很久以前,在那个小镇里,朋友 会帮助我们寻找商品——那块布料很适合我;那本新书我很喜欢;那 台迷你留声机很棒。即便在今天,我们也非常看重朋友的推荐。 我们还会请 专家 帮助我们寻找商品。 过去,消费者周刊能够对所有的洗衣机(20种)和电饭煲(10种)做出评测,并进行推荐;但如今, 亚马逊上有上百种电饭煲,不是一个专家就能评测完全的。 过去,影评家艾伯特几乎能够对所有的电影进行评判;但如今,每年都有两万五千部电影在世界各地 上映。此外,我们还能通过各种途径获取到视频节目。艾伯特也好,其他影评家也罢,是不可能对所 有的电影做出评价的。 此外,我们还会通过 商品本身 来寻找。比如,我有一台用了三十年的希尔士洗衣机,所以我会再去 购买一台同品牌的洗衣机;我喜欢披头士的一张专辑,所以会认为他们的另一张专辑也很有吸引力。 这些寻找商品的方式可以沿用至今,但是我们需要用电算化的手段让这些方法能够适用于21世纪的商 品数量。 本书将会探索这些方法,将人们的喜恶收集起来,分析他们的购买历史,发掘社会网络(朋友)的数 据价值,从而帮助我们找到相关的商品。比方说,我喜欢Phoenix乐队,那系统会使用这个乐队的一 些特点——重金属、朋克、和声——来推荐其他的乐队给我,如The Strokes乐队。 不仅仅是寻找商品 数据挖掘不仅仅是用来推荐商品,或是单单给商人增加销量的。看看下面的示例。 回到一百年前的那个小镇,镇长在竞选演讲上可以针对每个选民来给出承诺:玛莎,我知道你对教育 本文档使用 书栈(BookStack.CN) 构建 - 14 -

15.欢迎来到21世纪 事业非常在意,我会尽一切努力去招募另一名教师到我们小镇来;约翰,你的面包房经营得如何?我 会在你的商店周围建造更多的停车场的。 我父亲是联合汽车工会的成员。在选举期间,工会的代表曾来到我家,游说我父亲要投票给谁: 赛尔,你好。你的家人和孩子都好吧?……现在让我来告诉你为什么要投票给赛德勒,让这位社会学家当选市长。 赛德勒是1948至1960年密尔沃基市的市长。 随着电视的普及,这类个性化的推广信息逐渐转变为广告形式,每个人得到的信息都是一样的,其中 一个著名的示例是为支持约翰逊竞选的黛西广告(一个小女孩在雏菊花田里骑着单车,此时一枚核弹 从天而降)。 现在,随着得票率相差得越来越小,以及数据挖掘技术的应用推广,个性化的竞选广告又回来了。比 如你对女权主义很在意,也许就会接听到一个关于这方面信息的语音电话。 本文档使用 书栈(BookStack.CN) 构建 - 15 -

16.欢迎来到21世纪 那个小镇的警官非常清楚谁是制造麻烦的人。而如今,各类威胁是隐秘起来的,恐怖主义随处可能发 生。 2001年10月11日,政府通过了《美国爱国者法案》(USA Patriot Act,意为提供合适的工具来截 获恐怖主义的相关信息,从而保护美国公民)。这项法案的条款之一是调查者能够通过各种渠道来获 得信息,比如图书馆借阅记录、旅馆出入记录、信用卡信息、公路收费站记录等等。 美国政府通过和某些私营企业合作,收集我们的各项信息。比如赛新公司持有几乎所有人的记录,我 们的照片、住址、座驾、收入、消费习惯、朋友等。赛新拥有的超级计算机系统能够通过数据挖掘来 预测人们的行为。他们的产品有一个响亮的名字: 矩阵 本文档使用 书栈(BookStack.CN) 构建 - 16 -

17.欢迎来到21世纪 数据挖掘扩展了我们的能力 贝克在他的作品《数学奇才》中写道: 想象你正在一家咖啡馆,可能十分嘈杂。一位年轻的女士坐在你的右侧,正在操作笔记本电脑。你转过头去,看着她的屏幕。她正在 上网。你开始观察。 几个小时过去了,她先是阅读了一篇在线论文,然后读了三篇关于中国的文章;她浏览了周五晚上会上映的电影,还看了一篇功夫熊 猫的影评;她点击了一个广告,说是能帮助用户找到自己的老同学。你在那里看着她操作,并记录下来。每过一分钟,你对她的了解 就多一分。 好,现在想象一下你可以同时看着1500万人的电脑屏幕,记录他们的操作。 数据挖掘的重点在于找到数据中的模式。对于少量的数据,我们非常擅长在大脑中构建模型,搜寻模 式。 比如,今晚我想和妻子看一部电影,我很清楚她喜欢什么类型的电影。我知道她不喜欢含有暴力元素 的电影(这就是她不喜欢第九区的原因),她喜欢卡夫曼的电影。我可以利用这些信息来预测她会对 什么电影感兴趣。 一位欧洲的朋友远道而来,我知道她是一位素食主义者,所以我能猜到她一定不会喜欢我们当地的烤 肋排。 人们非常善于利用已有信息来进行预测。数据挖掘则扩展了我们的能力,让我们能够处理海量的数 据,比如我上文提到的1500万人的示例。数据挖掘能让潘多拉音乐站提供个性化的音乐列表;它能让 Netflix将你最感兴趣的视频推荐给你。 海量数据挖掘不是星际争霸II才有的东西 20世纪末,百万单词的数据已经是很大的量了。我于1990年代毕业(没错,我已经很老了),有一年 我作为程序员在研究新约圣经,虽然只有20万字,但仍无法完整地放入主机内存,所以只能将计算结 果不断地写入磁带中,而磁带的装卸是需要经过批准的。 本文档使用 书栈(BookStack.CN) 构建 - 17 -

18.欢迎来到21世纪 这次的研究成果汇集成了一本书,名为《Analytical Greek New Testament》 ,由T.福利伯格 和B.福利伯格编写。我是当时的三名程序员之一,在明尼苏达大学完成的研究。 如今,在TB级别的数据量上做挖掘已经很常见了。 谷歌有超过5PB的页面数据(即5000TB)。2006年,谷歌向研究者社区开放了一万亿单词量的数据 集。美国国家安全局有着上万亿的电话录音数据。Acxiom,这家做数据采集的公司(信用卡消费记 录、电话通信记录、医疗记录、车辆登记等),有着全美两亿成年人的信息,共计超过1PB的数据。 本文档使用 书栈(BookStack.CN) 构建 - 18 -

19.欢迎来到21世纪 图为包含了1PB数据的服务器集装箱。 《无处可藏》的作者欧哈罗曾试图帮助我们理解1PB的数据是什么样的概念,说这些数据相当于5万公 里的钦定版圣经的长度。我经常往返于新墨西哥州和弗吉尼亚州,两地相距两万公里,于是我便可以 想象一路上看到的全是这些书籍,数据量可见之大。 美国国会图书馆有大学20TB的文字,你可以将这些文字全部放入仅需几千美金的硬盘中。相对地,沃 尔玛则有超过570TB的数据。这些数据不只是存放在那儿,而是不断有人对其进行挖掘,找到新的关 联、新的模式。这就是海量数据挖掘! 本书中我们只会处理很小量的数据,这是好事,因为我们不希望自己的代码运行了一整周后发现其中 有一个逻辑错误。我们会处理的最大数据量也在百兆以下,最小的数据集则只有几十行。 本文档使用 书栈(BookStack.CN) 构建 - 19 -

20.本书的结构 本书的结构 本书的结构 读完本书后你将能够做些什么事? 标题中的“数学奇才的古老艺术”有什么含义? 本书的结构 这本书按照边学边做的原则编写。与其被动地接受书中的内容,我建议读者使用书中提供的Python代 码来进行实践。尝试各种算法,做一些修改,使用不同的数据集查看效果,从而真正地掌握这些知识 和技术。 我会尝试在简单易懂的Python代码和其背后的算法逻辑之间找到平衡点。为了避免读者们为各种理 论、数学公式、以及Python代码绞尽脑汁,我会增加图表和插画来做调剂。 谷歌研究院总监诺维格曾在他的Udacity课程《计算机程序设计》中写道: 我会向你展示和讨论我的解决方案。但需要注意的是,解决问题的方案不止一个。并不是说我的方案是 唯一的 或 最好的 。我的方 案不过是帮助你学习编程的一种风格和技术。如果你用另一种方式解决了问题,那会非常好。 所有的学习过程都是在你的头脑中发生的,而不是我的。所以你需要非常了解你的代码和我的代码之间的关系——你需要自己编写出答 案,然后从我的代码中挑选出有用的部分来学习和借鉴。 我非常赞同这个观点 本文档使用 书栈(BookStack.CN) 构建 - 20 -

21.本书的结构 图文:用血和汗水来编程! 这本书并不是一本完整论述数据挖掘技术的教科书。市面上有一些这样的教科书,如由谭恩、 斯坦巴克、以及库马合著的《数据挖掘导论》,就很全面地讲解了数据挖掘的各种理论,及其背后的 数学知识。 而你正在阅读的这本书,只是帮助你快速了解数据挖掘的基础理论,并进行实践。读完本书后,你可 以再找一本完整的教科书来填补空白。 这本书另一个比较实用的地方是它所提供的Python代码和数据集。我认为这可以帮助读者更快速地掌 握数据挖掘的核心思想,而又不会陷得太深,事倍功半。 读完本书后你将能够做些什么事? 读完本书后,你将有能力使用Python或其它编程语言,为一个网站设计和实现一套推荐系统。 例如,你在亚马逊上浏览一件商品,或者在潘多拉上聆听一首音乐,你可得到一组相关产品的列表 (也就是“猜你喜欢”)。你会学到如何开发出这样一套系统。 此外,这部分书提到的相关术语可以让你能够顺畅地与数据挖掘团队作沟通。 作为目标的一部分,本书还将为你揭开推荐系统的神秘面纱,包括那些恐怖分子识别系统及其他数据 挖掘系统,至少你将知道这些系统是怎么运作的。 为什么这点很重要? 为什么你需要花时间来阅读本书呢?在本章的开始,我给出了很多示例来说明数据挖掘的重要性。 本文档使用 书栈(BookStack.CN) 构建 - 21 -

22.本书的结构 那段文字可以转述如下:市场上有很多商品(电影、音乐、书籍、烹饪器具),而且数量在不断增 加,随之而来的问题便是如何在这么多商品中找到我们最感兴趣的——那么多电影我该看哪部?我接下 来应该读哪本书?数据挖掘就是用来解决这类问题的。 大多数网站都会提供查找商品的功能,除了上面提到的商品,你还会考虑该关注哪位好友;是否能够 有一份报纸只刊登你感兴趣的文章?如果你是一名Web开发者,就非常需要了解数据挖掘方面的知识 了。 好,现在你应该了解为什么要花时间来学习数据挖掘了,但为何要选择这本书呢? 市面上有些书籍是非技术类的,描述了数据挖掘的大致情况。这些书可以快速翻阅,十分有趣,而且 不贵,很适合深夜阅读(因为没有繁杂的技术细节)。 这类书籍的最佳代表是贝克的《数学奇才》,我非常推荐这本书。在我往来弗吉尼亚和新墨西哥时就 听的是这本书的语音版。另一个极端则是数据挖掘教学中使用的教科书。这些书籍涵盖面广,将数据 挖掘的理论和实践讲解得非常透彻,所以我也推荐阅读这类书籍。 至于这本书,则是用来填补这两者之间的空白的。本书的目标读者是那些喜欢编程的骇客们。 这本书应该在电脑前阅读,这样读者就可以立刻编写代码参与其中。 天呐,这是什么?这本书会包含一些数学公式,不过我会用一种简明的方式表述,相信普通的程序员 本文档使用 书栈(BookStack.CN) 构建 - 22 -

23.本书的结构 都能了解,即便你已经忘记了大学中学习数学知识。 如果以上这些都不能说服你,那还有一点:这本书是免费的,你可以随意分享它。 标题中的“数学奇才的古老艺术”有什么含义? 2010年6月,我曾尝试给这本书起一个合适的标题。我喜欢有趣的标题,但很可惜我不太擅长起名 字。 近期,我发表了篇关于数据挖掘的论文,名为《扎入文字堆:阿拉伯文字的地域化分类》。我喜欢这 个标题,不过我得承认这是我的太太帮我取的。 我曾和马克肖恩合著了一篇论文,名为《情绪与模式:从理论到争辩》,这个标题也是我的搭档取 的。 总之,六月时我取的那些标题很难一眼看出这本书讲的是什么,所以我最后用了《面向程序员的数据 挖掘指南》作为标题的一部分,因为这个标题和本书的内容非常契合——这本书是提供给正在从事编程 工作的人员阅读的。也许你会疑惑子标题究竟是什么意思: 数学奇才(Numerati)是贝克自己创造的一个词语。如今,我们每个人无时无刻不在创造着新的数 据,信用卡购物记录、推特、格瓦拉上的博客、Foursquare上的签到、手机通话记录、电子邮件、 文字短信等。 当你一早醒来,“矩阵”就知道你会乘坐雾谷站7:10的地铁,并于7:32在西站下车;矩阵知道7:45分 你会去第五大街的星巴克买上一杯大杯拿铁和一份蓝莓饼;8:05分,你用格瓦拉在上班地点签到; 9:35分,你在亚马逊上购买了一套瘦身教程DVD和一副门上单杠;你在Golden Falafel吃的午餐。 贝克在书中这样写道: 只有那些数学家、计算机科学家、以及工程师们才能从这些庞大的数据集中获得有用的信息。这些数学奇才会从这些数据中了解到什 么?首先,他们能够准确地定位到我们。比如你是纽约北部市郊的一个潜在的SUV客户,或是一个经常去教堂做礼拜的人,或是阿尔伯 克基市的一名反堕胎的民主党人士;也许你是一个即将被调任到海得拉巴市的一名Java工程师,或是一个热爱爵士乐的人;你是射手 座的,喜欢喝勤地酒,想在乡野间漫步,最后在斯德哥尔摩的篝火旁酣睡;更夸张的,也许你腰绑炸弹,正乘上一部公交车。无论你 是谁,处在茫茫人海中,那些公司或政府机构都能掌握你的行踪。 你可能猜到了,起这个标题是因为我喜欢贝克的这段描述。 本文档使用 书栈(BookStack.CN) 构建 - 23 -

24.本书的结构 本文档使用 书栈(BookStack.CN) 构建 - 24 -

25.推荐系统入门 推荐系统入门 第二章:推荐系统入门 第二章:推荐系统入门 原文:http://guidetodatamining.com/chapter2/ 本章将介绍协同过滤,基本的距离算法,包括曼哈顿距离、欧几里得距离、闵科夫斯基距离、皮尔森 相关系数。使用Python实现一个基本的推荐算法。 内容: 推荐系统工作原理 社会化协同过滤工作原理 如何找到相似物品 曼哈顿距离 欧几里得距离 闵可夫斯基距离 皮尔逊相关系数 余弦相似度 使用Python实现K最邻近算法 图书漂流站(BookCrossing)数据集 本文档使用 书栈(BookStack.CN) 构建 - 25 -

26.你喜欢的东西我也喜欢 你喜欢的东西我也喜欢 你喜欢的东西我也喜欢 如何找到相似的用户? 曼哈顿距离 欧几里得距离 N维模型 推广:闵可夫斯基距离 你喜欢的东西我也喜欢 我们将从推荐系统开始,开启数据挖掘之旅。推荐系统无处不在,如亚马逊网站的“看过这件商品的顾 客还购买过”板块: last.fm上对音乐和演唱会的推荐(相似歌手): 本文档使用 书栈(BookStack.CN) 构建 - 26 -

27.你喜欢的东西我也喜欢 在亚马逊的例子里,它用了两个元素来进行推荐:一是我浏览了里维斯翻译的《法华经》一书;二是 其他浏览过该书的顾客还浏览过的译作。 本章我们讲述的推荐方法称为协同过滤。顾名思义,这个方法是利用他人的喜好来进行推荐,也就是 说,是大家一起产生的推荐。 他的工作原理是这样的:如果要推荐一本书给你,我会在网站上查找一个和你类似的用户,然后将他 喜欢的书籍推荐给你——比如巴奇加卢比的《发条女孩》。 如何找到相似的用户? 所以首先要做的工作是找到相似的用户。这里用最简单的二维模型来描述。 假设用户会在网站用五颗星来评价一本书——没有星表示书写得很糟,五颗星表示很好。因为我们用的 是二维模型,所以仅对两本书进行评价:史蒂芬森的《雪崩》(纵轴)和拉尔森的《龙纹身的女孩》 (横轴)。 本文档使用 书栈(BookStack.CN) 构建 - 27 -

28.你喜欢的东西我也喜欢 首先,下表显示有三位用户对这两本书做了评价: 现在我想为神秘的X先生推荐一本书,他给《雪崩》打了四星,《龙纹身的女孩》两星。第一个任务是 找出哪个用户和他最为相似。我们用距离来表示。 曼哈顿距离 最简单的距离计算方式是曼哈顿距离。在二维模型中,每个人都可以用(x, y)的点来表示,这里我用 下标来表示不同的人,(x1, y1)表示艾米,(x2, y2)表示那位神秘的X先生,那么他们之间的曼哈 顿距离就是: 也就是x之差的绝对值加上y之差的绝对值,这样他们的距离就是4。 本文档使用 书栈(BookStack.CN) 构建 - 28 -

29.你喜欢的东西我也喜欢 完整的计算结果如下: 艾米的距离最近,在她的浏览历史中可以看到她曾给巴奇加卢比的《发条女孩》打过五星,于是我们 就可以把这本书推荐给X先生。 欧几里得距离 曼哈顿距离的优点之一是计算速度快,对于Facebook这样需要计算百万用户之间的相似度时就非常 有利。 勾股定理 也许你还隐约记得勾股定理。另一种计算距离的方式就是看两点之间的直线距离: 本文档使用 书栈(BookStack.CN) 构建 - 29 -

30.你喜欢的东西我也喜欢 利用勾股定理,我们可以如下计算距离: 这条斜线就是欧几里得距离,公式是: 回顾一下,这里的x1表示用户1喜欢《龙纹身》的程度,x2是用户2喜欢这本书的程度;y1则是用户1 喜欢《雪崩》的程度,y2是用户2喜欢这本书的程度。 艾米给《龙纹身》和《雪崩》都打了五颗星,神秘的X先生分别打了两星和四星,这样他们之间的欧几 里得距离就是: 以下是全部用户的计算结果: 本文档使用 书栈(BookStack.CN) 构建 - 30 -

31.你喜欢的东西我也喜欢 N维模型 刚才我们仅仅对两本书进行评价(二维模型),下面让我们扩展一下,尝试更复杂的模型。 假设我们现在要为一个在线音乐网站的用户推荐乐队。用户可以用1至5星来评价一个乐队,其中包含 半星(如2.5星)。下表展示了8位用户对8支乐队的评价: 表中的短横表示这位用户没有给这支乐队打分。我们在计算两个用户的距离时,只采用他们都评价过 的乐队,比如要计算Angelica和Bill的距离,我们只会用到5支乐队。这两个用户的曼哈顿距离 为: 本文档使用 书栈(BookStack.CN) 构建 - 31 -

32.你喜欢的东西我也喜欢 最后距离即是上方数据的加和:(1.5 + 1.5 + 3 + 2 + 1)。 计算欧几里得距离的方法也是类似的,我们也只取双方都评价过的乐队。 用公式来描述即: 掌握了吗? 那就试试计算其他几个用户之间的距离吧。 本文档使用 书栈(BookStack.CN) 构建 - 32 -

33.你喜欢的东西我也喜欢 有个瑕疵 当我们计算Hailey和Veronica的距离时会发现一个问题:他们共同评价的乐队只有两支(Norah Jones和The Strokes),而Hailey和Jordyn共同评价了五支乐队,这似乎会影响我们的计算结 果,因为Hailey和Veronica之间是二维的,而Haily和Veronica之间是五维的。 曼哈顿距离和欧几里得距离在数据完整的情况下效果最好。如何处理缺失数据,这在研究领域仍是一 个活跃的话题。本书的后续内容会进行一些讨论,这里先不展开。现在,让我们开始构建一个推荐系 统吧。 推广:闵可夫斯基距离 我们可以将曼哈顿距离和欧几里得距离归纳成一个公式,这个公式称为闵可夫斯基距离: 其中: r = 1 该公式即曼哈顿距离 r = 2 该公式即欧几里得距离 本文档使用 书栈(BookStack.CN) 构建 - 33 -

34.你喜欢的东西我也喜欢 r = ∞ 极大距离 当你在书中看到这些数学公式,你可以选择快速略过它,继续读下面的文字,过去我就是这样;你也 可以停下来,好好分析一下这些公式,会发现其实它们并不难理解。 比如上面的公式,当r = 1时,可以简化成如下形式: 仍用上文的音乐站点为例,x和y分别表示两个用户,d(x, y)表示他们之间的距离,n表示他们共同 评价过的乐队数量,我们之前已经做过计算: 其中Difference一栏表示两者评分之差的绝对值,加起来等于9,也就是他们之间的距离。 当r = 2时,我们得到欧几里得距离的计算公式: 本文档使用 书栈(BookStack.CN) 构建 - 34 -

35.你喜欢的东西我也喜欢 提前预告一下:r值越大,单个维度的差值大小会对整体距离有更大的影响。 本文档使用 书栈(BookStack.CN) 构建 - 35 -

36.使用Python代码来表示数据 使用Python代码来表示数据 使用Python代码来表示数据(终于要开始编程了) 计算曼哈顿距离 用户的问题 皮尔逊相关系数 使用Python代码来表示数据(终于要开始编程 了) 在Python中,我们可以用多种方式来描述上表中的数据,这里我选择Python的字典类型(或者称为 关联数组、哈希表)。 注:本书的所有代码可以在这里找到。 1. users = {"Angelica": {"Blues Traveler": 3.5, "Broken Bells": 2.0, "Norah Jones": 4.5, "Phoenix": 5.0, "Slightly Stoopid": 1.5, "The Strokes": 2.5, "Vampire Weekend": 2.0}, 2. "Bill":{"Blues Traveler": 2.0, "Broken Bells": 3.5, "Deadmau5": 4.0, "Phoenix": 2.0, "Slightly Stoopid": 3.5, "Vampire Weekend": 3.0}, 3. "Chan": {"Blues Traveler": 5.0, "Broken Bells": 1.0, "Deadmau5": 1.0, "Norah Jones": 3.0, "Phoenix": 5, "Slightly Stoopid": 1.0}, 4. "Dan": {"Blues Traveler": 3.0, "Broken Bells": 4.0, "Deadmau5": 4.5, "Phoenix": 3.0, "Slightly Stoopid": 4.5, "The Strokes": 4.0, "Vampire Weekend": 2.0}, 5. "Hailey": {"Broken Bells": 4.0, "Deadmau5": 1.0, "Norah Jones": 4.0, "The Strokes": 4.0, "Vampire Weekend": 1.0}, 6. "Jordyn": {"Broken Bells": 4.5, "Deadmau5": 4.0, "Norah Jones": 5.0, "Phoenix": 5.0, "Slightly Stoopid": 4.5, "The Strokes": 4.0, "Vampire Weekend": 4.0}, 7. "Sam": {"Blues Traveler": 5.0, "Broken Bells": 2.0, "Norah Jones": 3.0, "Phoenix": 5.0, "Slightly Stoopid": 4.0, "The Strokes": 5.0}, 8. "Veronica": {"Blues Traveler": 3.0, "Norah Jones": 5.0, "Phoenix": 4.0, "Slightly Stoopid": 2.5, "The Strokes": 3.0} 9. } 我们可以用以下方式来获取某个用户的评分: 1. >>> users["Veronica"] 2. {"Blues Traveler": 3.0, "Norah Jones": 5.0, "Phoenix": 4.0, "Slightly Stoopid": 2.5, "The Strokes": 3.0} 3. >>> 计算曼哈顿距离 1. def manhattan(rating1, rating2): 2. """计算曼哈顿距离。rating1和rating2参数中存储的数据格式均为 本文档使用 书栈(BookStack.CN) 构建 - 36 -

37.使用Python代码来表示数据 3. {'The Strokes': 3.0, 'Slightly Stoopid': 2.5}""" 4. distance = 0 5. for key in rating1: 6. if key in rating2: 7. distance += abs(rating1[key] - rating2[key]) 8. return distance 我们可以做一下测试: 1. >>> manhattan(users['Hailey'], users['Veronica']) 2. 2.0 3. >>> manhattan(users['Hailey'], users['Jordyn']) 4. 7.5 5. >>> 下面我们编写一个函数来找出距离最近的用户(其实该函数会返回一个用户列表,按距离排序): 1. def computeNearestNeighbor(username, users): 2. """计算所有用户至username用户的距离,倒序排列并返回结果列表""" 3. distances = [] 4. for user in users: 5. if user != username: 6. distance = manhattan(users[user], users[username]) 7. distances.append((distance, user)) 8. # 按距离排序——距离近的排在前面 9. distances.sort() 10. return distances 简单测试一下: 1. >>> computeNearestNeighbor("Hailey", users) 2. [(2.0, 'Veronica'), (4.0, 'Chan'), (4.0, 'Sam'), (4.5, 'Dan'), (5.0, 'Angelica'), (5.5, 'Bill'), (7.5, 'Jordyn')] 最后,我们结合以上内容来进行推荐。 假设我想为Hailey做推荐,这里我找到了离他距离最近的用户Veronica。然后,我会找到出 Veronica评价过但Hailey没有评价的乐队,并假设Hailey对这些陌生乐队的评价会和Veronica相 近。 比如,Hailey没有评价过Phoenix乐队,而Veronica对这个乐队打出了4分,所以我们认为Hailey 也会喜欢这支乐队。下面的函数就实现了这一逻辑: 1. def recommend(username, users): 2. """返回推荐结果列表""" 本文档使用 书栈(BookStack.CN) 构建 - 37 -

38.使用Python代码来表示数据 3. # 找到距离最近的用户 4. nearest = computeNearestNeighbor(username, users)[0][1] 5. recommendations = [] 6. # 找出这位用户评价过、但自己未曾评价的乐队 7. neighborRatings = users[nearest] 8. userRatings = users[username] 9. for artist in neighborRatings: 10. if not artist in userRatings: 11. recommendations.append((artist, neighborRatings[artist])) 12. # 按照评分进行排序 13. return sorted(recommendations, key=lambda artistTuple: artistTuple[1], reverse = True) 下面我们就可以用它来为Hailey做推荐了: 1. >>> recommend('Hailey', users) 2. [('Phoenix', 4.0), ('Blues Traveler', 3.0), ('Slightly Stoopid', 2.5)] 运行结果和我们的预期相符。我们看可以看到,和Hailey距离最近的用户是Veronica,Veronica 对Phoenix乐队打了4分。我们再试试其他人: 1. >>> recommend('Chan', users) 2. [('The Strokes', 4.0), ('Vampire Weekend', 1.0)] 3. >>> recommend('Sam', users) 4. [('Deadmau5', 1.0)] 我们可以猜想Chan会喜欢The Strokes乐队,而Sam不会太欣赏Deadmau5。 1. >>> recommend('Angelica', users) 2. [] 对于Angelica,我们得到了空的返回值,也就是说我们无法对其进行推荐。让我们看看是哪里有问 题: 1. >>> computeNearestNeighbor('Angelica', users) 2. [(3.5, 'Veronica'), (4.5, 'Chan'), (5.0, 'Hailey'), (8.0, 'Sam'), (9.0, 'Bill'), (9.0, 'Dan'), (9.5, 'Jordyn')] Angelica最相似的用户是Veronica,让我们回头看看数据: 本文档使用 书栈(BookStack.CN) 构建 - 38 -

39.使用Python代码来表示数据 我们可以看到,Veronica评价过的乐队,Angelica也都评价过了,所以我们没有推荐。 之后,我们会讨论如何解决这一问题。 作业:实现一个计算闵可夫斯基距离的函数,并在计算用户距离时使用它。 1. def minkowski(rating1, rating2, r): 2. distance = 0 3. for key in rating1: 4. if key in rating2: 5. distance += pow(abs(rating1[key] - rating2[key]), r) 6. return pow(distance, 1.0 / r) 7. 8. # 修改computeNearestNeighbor函数中的一行 9. distance = minkowski(users[user], users[username], 2) 10. # 这里2表示使用欧几里得距离 用户的问题 让我们仔细看看用户对乐队的评分,可以发现每个用户的打分标准非常不同: Bill没有打出极端的分数,都在2至4分之间; Jordyn似乎喜欢所有的乐队,打分都在4至5之间; Hailey是一个有趣的人,他的分数不是1就是4。 那么,如何比较这些用户呢?比如Hailey的4分相当于Jordan的4分还是5分呢?我觉得更接近5分。 这样一来就会影响到推荐系统的准确性了。 本文档使用 书栈(BookStack.CN) 构建 - 39 -

40.使用Python代码来表示数据 左:我非常喜欢Broken Bells乐队,所以我给他们打4分! 右:Broken Bells乐队还可以,我打4分。 皮尔逊相关系数 解决方法之一是使用皮尔逊相关系数。简单起见,我们先看下面的数据(和之前的数据不同): 这种现象在数据挖掘领域称为“分数膨胀”。Clara最低给了4分——她所有的打分都在4至5分之间。我 们将它绘制成图表: 本文档使用 书栈(BookStack.CN) 构建 - 40 -

41.使用Python代码来表示数据 一条直线——完全吻合!!! 直线即表示Clara和Robert的偏好完全一致。他们都认为Phoenix是最好的乐队,然后是Blues Traveler、Norah Jones。如果Clara和Robert的意见不一致,那么落在直线上的点就越少。 意见基本一致的情形 意见不太一致的情形 本文档使用 书栈(BookStack.CN) 构建 - 41 -

42.使用Python代码来表示数据 所以从图表上理解,意见相一致表现为一条直线。 皮尔逊相关系数用于衡量两个变量之间的相关性(这里的两个变量指的是Clara和Robert),它的值 在-1至1之间,1表示完全吻合,-1表示完全相悖。 从直观上理解,最开始的那条直线皮尔逊相关系数为1,第二张是0.91,第三张是0.81。因此我们利 用这一点来找到相似的用户。 皮尔逊相关系数的计算公式是: 本文档使用 书栈(BookStack.CN) 构建 - 42 -

43.使用Python代码来表示数据 这里我说说自己的经历。我大学读的是现代音乐艺术,课程包括芭蕾、现代舞、服装设计等,没有任 何数学课程。 我高中读的是男子学校,学习了管道工程和汽车维修,只懂得很基础的数学知识。不知是因为我的学 科背景,还是习惯于用直觉来思考,当我遇到这样的数学公式时会习惯性地跳过,继续读下面的文 字。 如果你和我一样,我强烈建议你与这种惰性抗争,试着去理解这些公式。它们虽然看起来很复杂,但 还是能够被常人所理解的。 上面的公式除了看起来比较复杂,另一个问题是要获得计算结果必须对数据做多次遍历。好在我们有 另外一个公式,能够计算皮尔逊相关系数的近似值: 这个公式虽然看起来更加复杂,而且其计算结果会不太稳定,有一定误差存在,但它最大的优点是, 用代码实现的时候可以只遍历一次数据,我们会在下文看到。 首先,我们将这个公式做一个分解,计算下面这个表达式的值: 对于Clara和Robert,我们可以得到: 很简单把?下面我们计算这个公式: 本文档使用 书栈(BookStack.CN) 构建 - 43 -

44.使用Python代码来表示数据 Clara的总评分是22.5, Robert是15,他们评价了5支乐队,因此: 所以,那个巨型公式的分子就是70 - 67.5 = 2.5。 下面我们来看分母: 首先: 我们已经计算过Clara的总评分是22.5,它的平方是506.25,除以乐队的数量5,得到101.25。综 合得到: 对于Robert,我们用同样的方法计算: 本文档使用 书栈(BookStack.CN) 构建 - 44 -

45.使用Python代码来表示数据 最后得到: 因此,1表示Clara和Robert的偏好完全吻合。 先休息一下吧 计算皮尔逊相关系数的代码 1. from math import sqrt 2. 3. def pearson(rating1, rating2): 4. sum_xy = 0 5. sum_x = 0 6. sum_y = 0 7. sum_x2 = 0 8. sum_y2 = 0 9. n = 0 10. for key in rating1: 11. if key in rating2: 12. n += 1 13. x = rating1[key] 本文档使用 书栈(BookStack.CN) 构建 - 45 -

46.使用Python代码来表示数据 14. y = rating2[key] 15. sum_xy += x * y 16. sum_x += x 17. sum_y += y 18. sum_x2 += pow(x, 2) 19. sum_y2 += pow(y, 2) 20. # 计算分母 21. denominator = sqrt(sum_x2 - pow(sum_x, 2) / n) * sqrt(sum_y2 - pow(sum_y, 2) / n) 22. if denominator == 0: 23. return 0 24. else: 25. return (sum_xy - (sum_x * sum_y) / n) / denominator 测试一下: 1. >>> pearson(users['Angelica'], users['Bill']) 2. -0.9040534990682699 3. >>> pearson(users['Angelica'], users['Hailey']) 4. 0.42008402520840293 5. >>> pearson(users['Angelica'], users['Jordyn']) 6. 0.7639748605475432 好,让我们继续~ 本文档使用 书栈(BookStack.CN) 构建 - 46 -

47.最后一个公式:余弦相似度 最后一个公式:余弦相似度 最后一个公式:余弦相似度 应该使用哪种相似度? 古怪的现象 K最邻近算法 最后一个公式:余弦相似度 这里我将奉上最后一个公式:余弦相似度。它在文本挖掘中应用得较多,在协同过滤中也会使用到。 为了演示如何使用该公式,我们换一个示例。这里记录了每个用户播放歌曲的次数,我们用这些数据 进行推荐: 简单扫一眼上面的数据(或者用之前讲过的距离计算公式),我们可以发现Ann的偏好和Sally更为 相似。 问题在哪儿? 我在iTunes上有大约4000首歌曲,下面是我最常听的音乐: 本文档使用 书栈(BookStack.CN) 构建 - 47 -

48.最后一个公式:余弦相似度 可以看到,Moonlight Sonata这首歌我播放了25次,但很有可能你一次都没有听过。 事实上,上面列出的这些歌曲可能你一首都没听过。此外,iTunes上有1500万首音乐,而我只听过 4000首。所以说单个用户的数据是 稀疏 的,因为非零值较总体要少得多。 当我们用1500万首歌曲来比较两个用户时,很有可能他们之间没有任何交集,这样一来就无从计算他 们之间的距离了。 类似的情况是在计算两篇文章的相似度时。 比如说我们想找一本和《The Space Pioneers》相类似的书,方法之一是利用单词出现的频率, 即统计每个单词在书中出现的次数占全书单词的比例,如“the”出现频率为6.13%,“Tom” 0.89%,“space” 0.25%。我们可以用这些数据来寻找一本相近的书。 但是,这里同样有数据的稀疏性问题。《The Space Pioneers》中有6629个不同的单词,但英语 语言中有超过100万个单词,这样一来非零值就很稀少了,也就不能计算两本书之间的距离。 余弦相似度的计算中会略过这些非零值。它的计算公式是: 其中,“·”号表示数量积。“||x||”表示向量x的模,计算公式是: 本文档使用 书栈(BookStack.CN) 构建 - 48 -

49.最后一个公式:余弦相似度 我们用上文中“偏好完全一致”的示例: 所以两个向量为: 它们的模是: 数量积的计算: 因此余弦相似度是: 余弦相似度的范围从1到-1,1表示完全匹配,-1表示完全相悖。所以0.935表示匹配度很高。 作业:尝试计算Angelica和Veronica的余弦相似度 本文档使用 书栈(BookStack.CN) 构建 - 49 -

50.最后一个公式:余弦相似度 应该使用哪种相似度? 我们整本书都会探索这个问题,以下是一些提示: 如果数据存在“分数膨胀”问题,就使用皮尔逊相关系数。 如果数据比较“密集”,变量之间基本都存在公有值,且这些距离数据是非常重要的,那就使用欧 几里得或曼哈顿距离。 如果数据是稀疏的,则使用余弦相似度。 所以,如果数据是密集的,曼哈顿距离和欧几里得距离都是适用的。那么稀疏的数据可以使用吗?我 们来看一个也和音乐有关的示例:假设有三个人,每人都给100首音乐评过分。 本文档使用 书栈(BookStack.CN) 构建 - 50 -

51.最后一个公式:余弦相似度 Jake(左):乡村音乐的忠实听众。 Linda和Eric(右):我们爱六十年代的摇滚乐! Linda和Eric喜欢相同的音乐,他们的评分列表中有20首相同的的歌曲,且评分均值相差不到0.5! 所以他们之间的曼哈顿距离为20 x 0.5 = 10,欧几里得距离则为: Linda和Jake只共同评分了一首歌曲:Chris Cagle的 What a Beautiful Day 。Linda打了3 分,Jake打了5分,所以他们之间的曼哈顿距离为2,欧几里得距离为: 所以不管是曼哈顿距离还是欧几里得距离,Jake都要比Eric离Linda近,这不符合实际情况。 本文档使用 书栈(BookStack.CN) 构建 - 51 -

52.最后一个公式:余弦相似度 嘿,我想到一个办法。人们给音乐打分是从1到5分,那些没有打分的音乐就统一给0分好了,这样就能解决数据稀疏的问题了! 想法不错,但是这样做也不行。为了解释这一问题,我们再引入两个人到例子里来:Cooper和 Kelsey。他们和Jake都有着非常相似的音乐偏好,其中Jake在我们网站上评价了25首歌曲。 Cooper评价了26首歌曲,其中25首和Jake是一样的。他们对每首歌曲的评价差值只有0.25! Kelsey在我们网站上评价了150首歌曲,其中25首和Jake相同。和Cooper一样,她和Jake之间的 本文档使用 书栈(BookStack.CN) 构建 - 52 -

53.最后一个公式:余弦相似度 评价差值也只有0.25! 所以我们从直觉上看Cooper和Keylsey离Jake的距离应该相似。但是,当我们计算他们之间的曼哈 顿距离和欧几里得距离时(代入0值),会发现Cooper要比Keylsey离Jake近得多。 为什么呢? 我们来看下面的数据: 从4、5、6这三首歌来看,两人离Jake的距离是相同的,但计算出的曼哈顿距离却不这么显示: 问题就在于数据中的0值对结果的影响很大,所以用0代替空值的方法并不比原来的方程好。还有一种 变通的方式是计算“平均值”——将两人共同评价过的歌曲分数除以歌曲数量。 总之,曼哈顿距离和欧几里得距离在数据完整的情况下会运作得非常好,如果数据比较稀疏,则要考 虑使用余弦距离。 古怪的现象 假设我们要为Amy推荐乐队,她喜欢Phoenix、Passion Pit、以及Vampire Weekend。和她最相 似的用户是Bob,他也喜欢这三支乐队。他的父亲为Walter Ostanek乐队演奏手风琴,所以受此影 响,他给了这支乐队5星评价。按照我们现在的推荐逻辑,我们会将这支乐队推荐给Amy,但有可能她 并不喜欢。 本文档使用 书栈(BookStack.CN) 构建 - 53 -

54.最后一个公式:余弦相似度 或者试想一下,Billy Bob Olivera教授喜欢阅读数据挖掘方面的书籍以及科幻小说,他最邻近的 用户是我,因为我也喜欢这两种书。然而,我又是一个贵宾犬的爱好者,所以给《贵宾犬的隐秘生 活》这本书打了很高的分。这样一来,现有的推荐方法会将这本书介绍给Olivera教授。 问题就在于我们只依靠最相似的 一个 用户来做推荐,如果这个用户有些特殊的偏好,就会直接反映 在推荐内容里。解决方法之一是找寻多个相似的用户,这里就要用到K最邻近算法了。 本文档使用 书栈(BookStack.CN) 构建 - 54 -

55.最后一个公式:余弦相似度 K最邻近算法 在协同过滤中可以使用K最邻近算法来找出K个最相似的用户,以此作为推荐的基础。不同的应用有不 同的K值,需要做一些实验来得出。以下给到读者一个基本的思路。 假设我要为Ann做推荐,并令K=3。使用皮尔逊相关系数得到的结果是: 这三个人都会对推荐结果有所贡献,问题在于我们如何确定他们的比重呢? 我们直接用相关系数的比重来描述,Sally的比重是0.8/2=40%,Eric是0.7/2=35%,Amanda则是 25%: 假设他们三人对Grey Wardens的评分以及加权后的结果如下: 最后计算得到的分数为: 本文档使用 书栈(BookStack.CN) 构建 - 55 -

56.最后一个公式:余弦相似度 本文档使用 书栈(BookStack.CN) 构建 - 56 -

57.Python推荐模块 Python推荐模块 Python推荐模块 新的数据集 项目实践 Python推荐模块 我将本章学到的内容都汇集成了一个Python类,虽然代码有些长,我还是贴在了这里: 1. import codecs 2. from math import sqrt 3. 4. users = {"Angelica": {"Blues Traveler": 3.5, "Broken Bells": 2.0, 5. "Norah Jones": 4.5, "Phoenix": 5.0, 6. "Slightly Stoopid": 1.5, 7. "The Strokes": 2.5, "Vampire Weekend": 2.0}, 8. 9. "Bill":{"Blues Traveler": 2.0, "Broken Bells": 3.5, 10. "Deadmau5": 4.0, "Phoenix": 2.0, 11. "Slightly Stoopid": 3.5, "Vampire Weekend": 3.0}, 12. 13. "Chan": {"Blues Traveler": 5.0, "Broken Bells": 1.0, 14. "Deadmau5": 1.0, "Norah Jones": 3.0, "Phoenix": 5, 15. "Slightly Stoopid": 1.0}, 16. 17. "Dan": {"Blues Traveler": 3.0, "Broken Bells": 4.0, 18. "Deadmau5": 4.5, "Phoenix": 3.0, 19. "Slightly Stoopid": 4.5, "The Strokes": 4.0, 20. "Vampire Weekend": 2.0}, 21. 22. "Hailey": {"Broken Bells": 4.0, "Deadmau5": 1.0, 23. "Norah Jones": 4.0, "The Strokes": 4.0, 24. "Vampire Weekend": 1.0}, 25. 26. "Jordyn": {"Broken Bells": 4.5, "Deadmau5": 4.0, 27. "Norah Jones": 5.0, "Phoenix": 5.0, 28. "Slightly Stoopid": 4.5, "The Strokes": 4.0, 29. "Vampire Weekend": 4.0}, 30. 31. "Sam": {"Blues Traveler": 5.0, "Broken Bells": 2.0, 32. "Norah Jones": 3.0, "Phoenix": 5.0, 33. "Slightly Stoopid": 4.0, "The Strokes": 5.0}, 34. 35. "Veronica": {"Blues Traveler": 3.0, "Norah Jones": 5.0, 本文档使用 书栈(BookStack.CN) 构建 - 57 -

58.Python推荐模块 36. "Phoenix": 4.0, "Slightly Stoopid": 2.5, 37. "The Strokes": 3.0} 38. } 39. 40. 41. class recommender: 42. 43. def __init__(self, data, k=1, metric='pearson', n=5): 44. """ 初始化推荐模块 45. data 训练数据 46. k K邻近算法中的值 47. metric 使用何种距离计算方式 48. n 推荐结果的数量 49. """ 50. self.k = k 51. self.n = n 52. self.username2id = {} 53. self.userid2name = {} 54. self.productid2name = {} 55. # 将距离计算方式保存下来 56. self.metric = metric 57. if self.metric == 'pearson': 58. self.fn = self.pearson 59. # 60. # 如果data是一个字典类型,则保存下来,否则忽略 61. # 62. if type(data).__name__ == 'dict': 63. self.data = data 64. 65. def convertProductID2name(self, id): 66. """通过产品ID获取名称""" 67. if id in self.productid2name: 68. return self.productid2name[id] 69. else: 70. return id 71. 72. def userRatings(self, id, n): 73. """返回该用户评分最高的物品""" 74. print ("Ratings for " + self.userid2name[id]) 75. ratings = self.data[id] 76. print(len(ratings)) 77. ratings = list(ratings.items()) 78. ratings = [(self.convertProductID2name(k), v) 79. for (k, v) in ratings] 80. # 排序并返回结果 81. ratings.sort(key=lambda artistTuple: artistTuple[1], 82. reverse = True) 83. ratings = ratings[:n] 本文档使用 书栈(BookStack.CN) 构建 - 58 -

59.Python推荐模块 84. for rating in ratings: 85. print("%s\t%i" % (rating[0], rating[1])) 86. 87. def loadBookDB(self, path=''): 88. """加载BX数据集,path是数据文件位置""" 89. self.data = {} 90. i = 0 91. # 92. # 将书籍评分数据放入self.data 93. # 94. f = codecs.open(path + "BX-Book-Ratings.csv", 'r', 'utf8') 95. for line in f: 96. i += 1 97. #separate line into fields 98. fields = line.split(';') 99. user = fields[0].strip('"') 100. book = fields[1].strip('"') 101. rating = int(fields[2].strip().strip('"')) 102. if user in self.data: 103. currentRatings = self.data[user] 104. else: 105. currentRatings = {} 106. currentRatings[book] = rating 107. self.data[user] = currentRatings 108. f.close() 109. # 110. # 将书籍信息存入self.productid2name 111. # 包括isbn号、书名、作者等 112. # 113. f = codecs.open(path + "BX-Books.csv", 'r', 'utf8') 114. for line in f: 115. i += 1 116. #separate line into fields 117. fields = line.split(';') 118. isbn = fields[0].strip('"') 119. title = fields[1].strip('"') 120. author = fields[2].strip().strip('"') 121. title = title + ' by ' + author 122. self.productid2name[isbn] = title 123. f.close() 124. # 125. # 将用户信息存入self.userid2name和self.username2id 126. # 127. f = codecs.open(path + "BX-Users.csv", 'r', 'utf8') 128. for line in f: 129. i += 1 130. #print(line) 131. #separate line into fields 本文档使用 书栈(BookStack.CN) 构建 - 59 -

60.Python推荐模块 132. fields = line.split(';') 133. userid = fields[0].strip('"') 134. location = fields[1].strip('"') 135. if len(fields) > 3: 136. age = fields[2].strip().strip('"') 137. else: 138. age = 'NULL' 139. if age != 'NULL': 140. value = location + ' (age: ' + age + ')' 141. else: 142. value = location 143. self.userid2name[userid] = value 144. self.username2id[location] = userid 145. f.close() 146. print(i) 147. 148. def pearson(self, rating1, rating2): 149. sum_xy = 0 150. sum_x = 0 151. sum_y = 0 152. sum_x2 = 0 153. sum_y2 = 0 154. n = 0 155. for key in rating1: 156. if key in rating2: 157. n += 1 158. x = rating1[key] 159. y = rating2[key] 160. sum_xy += x * y 161. sum_x += x 162. sum_y += y 163. sum_x2 += pow(x, 2) 164. sum_y2 += pow(y, 2) 165. if n == 0: 166. return 0 167. # 计算分母 168. denominator = (sqrt(sum_x2 - pow(sum_x, 2) / n) 169. * sqrt(sum_y2 - pow(sum_y, 2) / n)) 170. if denominator == 0: 171. return 0 172. else: 173. return (sum_xy - (sum_x * sum_y) / n) / denominator 174. 175. def computeNearestNeighbor(self, username): 176. """获取邻近用户""" 177. distances = [] 178. for instance in self.data: 179. if instance != username: 本文档使用 书栈(BookStack.CN) 构建 - 60 -

61.Python推荐模块 180. distance = self.fn(self.data[username], 181. self.data[instance]) 182. distances.append((instance, distance)) 183. # 按距离排序,距离近的排在前面 184. distances.sort(key=lambda artistTuple: artistTuple[1], 185. reverse=True) 186. return distances 187. 188. def recommend(self, user): 189. """返回推荐列表""" 190. recommendations = {} 191. # 首先,获取邻近用户 192. nearest = self.computeNearestNeighbor(user) 193. # 194. # 获取用户评价过的商品 195. # 196. userRatings = self.data[user] 197. # 198. # 计算总距离 199. totalDistance = 0.0 200. for i in range(self.k): 201. totalDistance += nearest[i][1] 202. # 汇总K邻近用户的评分 203. for i in range(self.k): 204. # 计算饼图的每个分片 205. weight = nearest[i][1] / totalDistance 206. # 获取用户名称 207. name = nearest[i][0] 208. # 获取用户评分 209. neighborRatings = self.data[name] 210. # 获得没有评价过的商品 211. for artist in neighborRatings: 212. if not artist in userRatings: 213. if artist not in recommendations: 214. recommendations[artist] = (neighborRatings[artist] 215. * weight) 216. else: 217. recommendations[artist] = (recommendations[artist] 218. + neighborRatings[artist] 219. * weight) 220. # 开始推荐 221. recommendations = list(recommendations.items()) 222. recommendations = [(self.convertProductID2name(k), v) 223. for (k, v) in recommendations] 224. # 排序并返回 225. recommendations.sort(key=lambda artistTuple: artistTuple[1], 226. reverse = True) 227. # 返回前n个结果 本文档使用 书栈(BookStack.CN) 构建 - 61 -

62.Python推荐模块 228. return recommendations[:self.n] 运行示例 首先构建一个推荐类,然后获取推荐结果: 1. >>> r = recommender(users) 2. >>> r.recommend('Jordyn') 3. [('Blues Traveler', 5.0)] 4. >>> r.recommend('Hailey') 5. [('Phoenix', 5.0), ('Slightly Stoopid', 4.5)] 新的数据集 现在让我们使用一个更为真实的数据集。Cai-Nicolas Zeigler从图书漂流站收集了超过100万条 评价数据——278,858位用户为271,379本书打了分。 这份数据(匿名)可以从这个地址获得,有SQL和CSV两种格式。由于特殊符号的关系,这些数据无法 直接加载到Python里。 我做了一些清洗,可以从这里下载。 CSV文件包含了三张表: 用户表,包括用户ID、位置、年龄等信息。其中用户的姓名已经隐去; 书籍表,包括ISBN号、标题、作者、出版日期、出版社等; 评分表,包括用户ID、书籍ISBN号、以及评分(0-10分)。 上文Python代码中的loadBookDB方法可以加载这些数据,用法如下: 1. >>> r.loadBookDB('/Users/raz/Downloads/BX-Dump/') 2. 1700018 3. >>> r.recommend('171118') 注意 由于数据集比较大,大约需要几十秒的时间加载和查询。 项目实践 只有运行调试过书中的代码后才能真正掌握这些方法,以下是一些实践建议: 1. 实现一个计算曼哈顿距离和欧几里得距离的方法; 2. 本书的网站上有一个包含25部电影评价的数据集,实现一个推荐算法。 本文档使用 书栈(BookStack.CN) 构建 - 62 -

63.Python推荐模块 本文档使用 书栈(BookStack.CN) 构建 - 63 -

64.隐式评价和基于物品的过滤算法 隐式评价和基于物品的过滤算法 第三章:隐式评价和基于物品的过滤算法 第三章:隐式评价和基于物品的过滤算法 原文:http://guidetodatamining.com/chapter3/ 本章会从用户的评价类型开始讨论,包括显式评价(赞一下、踩一脚、五星评价等等)和隐式评价 (比如在亚马逊上购买了MP3,我们可以认为他喜欢这个产品)。 内容: 显式评价 隐式评价 哪种评价方式更准确? 基于用户的协同过滤 基于物品的协同过滤 修正的余弦相似度 Slope One算法 Slope One的Python实现 MovieLens数据 第二章中我们学习了协同过滤和推荐系统的基本知识,其中讲述的算法是比较通用的,可以适用于多 种数据集。 用户使用5到10分的标尺来对不同的物品进行打分,通过计算得到相似的用户。但是,也有迹象表明 用户通常不会有效地使用这种度量方式,而更倾向于给出极好或极差的评价,这种做法会使推荐结果 变得不可用。 这一章我们将继续探讨这个问题,尝试使用高效的方法给出更精确的推荐。 本文档使用 书栈(BookStack.CN) 构建 - 64 -

65.显式评价 显式评价&隐式评价 显式评价 隐式评价 显式评价 用户的评价类型可以分为显式评价和隐式评价。显式评价指的是用户明确地给出对物品的评价,最常 见的例子是Pandora和YouTube上的“喜欢”和“不喜欢”按钮: 本文档使用 书栈(BookStack.CN) 构建 - 65 -

66.显式评价 以及亚马逊的星级系统: 隐式评价 所谓隐式评价,就是我们不让用户明确给出对物品的评价,而是通过观察他们的行为来获得偏好信 本文档使用 书栈(BookStack.CN) 构建 - 66 -

67.显式评价 息。示例之一是记录用户在纽约时报网上的点击记录。 经过几周的观察之后,我们就可以为用户刻画出一个合理的模型了——她不喜欢体育新闻,但关注科技 新闻;如果用户连续看了两篇文章:《快速减肥方法》和《不反弹的减肥方式》,那她很可能正在减 肥;如果她点击了iPhone的广告,就表明她或许对这款产品感兴趣。 试想一下,如果我们记录了用户在亚马逊上的操作记录,可以得出一些什么结论。你的首页上可能有 这样的内容: 在这个示例中,亚马逊记录了用户的点击操作,因此它会知道浏览了Jupter Travel这本书的用户 还浏览了Long Way Round这部DVD,其详细记录了演员伊万环球骑行的旅程。 因此,亚马逊就用这些信息来做出“看过还看过”的推荐。 另一种隐式评价是用户的实际购买记录,亚马逊也会用这些记录来进行“买过还买过”、以及“看过此 商品的用户还买过”的推荐。 本文档使用 书栈(BookStack.CN) 构建 - 67 -

68.显式评价 可能你会觉得“买过还买过”应该会给出一些不合理的推荐结果,但事实上它运作得很好。 再来看看iTunes上如何记录用户的行为: 首先,我将一首歌添加到了iTunes,这至少表明我对这首歌是感兴趣的。然后是播放次数,上表中我 听了Anchor这首歌52次,说明我很喜欢;而那些只听了一次的歌曲则是我不喜欢的。 头脑风暴 你觉得让用户对物品进行显式评价会更精确吗?还是说通过观察用户对物品的行为(是否购买或播放次数)才更为准确? 本文档使用 书栈(BookStack.CN) 构建 - 68 -

69.显式评价 显式评价:我叫吉姆,是一个素食主义者。我爱喝葡萄酒,喜欢在森林中漫步,在篝火旁阅读Chekov的书,喜欢观看法国电影,周六 会去艺术博物馆逛逛,我还喜欢舒曼的钢琴曲。 隐式评价:我们在吉姆的口袋里发现了12打美国蓝带啤酒的收银条,以及冰激淋、披萨和甜甜圈的收银条。还有一些租借DVD的回 执,有复仇者联盟、生化危机、拳霸等。 本文档使用 书栈(BookStack.CN) 构建 - 69 -

70.显式评价 本文档使用 书栈(BookStack.CN) 构建 - 70 -

71.显式评价的问题 显式评价的问题 显式评价的问题 问题1:人们很懒,不愿评价物品 问题2:人们会撒谎,或存有偏见 问题3:人们不会更新他们的评论 显式评价的问题 问题1:人们很懒,不愿评价物品 首先,用户很可能不会对物品做出评价。 相信各位读者已经在亚马逊上购买了很多商品,就拿我来说,仅过去一个月我就在那里购买了直升机 模型、1TB硬盘、USB-SATA转接头、维他命药片、两本Kindle电子书、四本纸质书。一共十件商 品,我评价了几件?零件!相信很多人和我是一样的——我们不评价商品,我们只管买。 我喜欢旅行和登山,所以购买了很多登山杖。亚马逊上一些价格实惠的登山杖很耐用。去年我到奥斯 汀市参加音乐会,途中碰坏了膝盖,于是到REI专营店买了一根价格昂贵的登山杖。不过这根杖居然 在我逛公园时用断了!这根昂贵的登山杖还没有买的10美元的来得结实。放假时,我打算给这件商品 写一篇评价,告诫其他购买者。结果呢?我没有写,因为我太懒了。 问题2:人们会撒谎,或存有偏见 我们假设有人不像前面说得那么懒,确实去给物品做出评价了,但他有可能会撒谎。 这种情况在前文中已经有提到了。用户可能会直接撒谎,给出不正确的评价;或是不置可否,抱有偏 见。 Ben和他的朋友们去看了一场泰国出的电影,Ben认为这部电影很糟糕,而其他人却觉得很好看,在餐 厅里欢快地谈论着。于是,Ben在评价电影时很有可能会抬高它的分数,这样才能表现得合群。 问题3:人们不会更新他们的评论 假设我去亚马逊评价了商品——那个1TB的硬盘速度很快也很静音;直升机模型操作起来也很简便,不 容易摔坏。所以这两件商品我都给出了5星的评价。但一个月后,那块硬盘坏了,我丢失了所有的电影 和音乐;那台直升机模型也突然不再工作了,让我非常扫兴。但是,我不太会返回亚马逊网站对这两 件商品的评价做出改动,这样人们依旧认为我是非常喜欢这两件商品的。 本文档使用 书栈(BookStack.CN) 构建 - 71 -

72.显式评价的问题 再举一个示例,玛丽很乐意在亚马逊上对商品做评价。她十年前给一些儿童类书籍打了很高的分数, 近些年又对一些摇滚乐队的专辑给出了评价。从近年的评价看,她和另一位用户珍妮很相似。但是, 如果我们把那些儿童书籍推荐给珍妮就显得不合适了。这个例子和上面的有些不同,但的确是个问 题。 头脑风暴 你觉得隐式评价会有什么问题?提示:可以回忆一下你在亚马逊的购买记录。 上文中我给出了一个近期在亚马逊上的购物列表,其中有两样是我买来送给其他人的。为什么这会是 一个问题?我再举一些其他的例子。我给我的孩子买了一个壶铃和一本关于健身的书籍;我给我的太 太买了一个边境牧羊犬的毛绒玩具,因为我家那只14岁大的狗去世了。通过隐式评价来进行建模,会 让你觉得我喜欢壶铃和毛绒玩具。亚马逊的购买记录无法区分这件商品是我买来自己用的还是送人 的。贝克也曾给出了相似的例子: 本文档使用 书栈(BookStack.CN) 构建 - 72 -

73.显式评价的问题 对于计算机来说,能够将白色连衣裙和婴儿潮出生的女性关联起来是任务的第一步,然后再对这些用户建立模型。假设我的太太在商 店里购买了几件商品:内衣、裤子、连衣裙、皮带等,这些商品都很符合婴儿潮的特点。离开时她想起要为自己16岁大的外甥女买一 件生日礼物。由于我们上次看到她时她穿着一件黑色的T恤,上面写满了文字,并自称是一名哥特摇滚妞。于是,我的太太就去买了一 根项圈准备送给她。 可以想象,如果我们要为这位用户构建模型,那这根项圈的存在就很有问题了。 再比如一对情侣使用的是同一个Netflix账号。男方喜欢各种爆破场面,女方则喜欢知性类型的电 影。如果我们从浏览历史进行挖掘,则会发现一个人会喜欢两种截然不同的影片类型。 前面说到我买了一些书给别人,所以单从购买历史看,同一本书我会购买很多次。这样有两种可能: 一是我的书不小心丢了,二是我得了老年痴呆,不记得自己曾读过这些书。而事实是我非常喜欢这些 书,因此多买了几本作为礼物来送给别人。所以说,用户的购买记录还是非常值得深究的。 头脑风暴 我们可以收集到哪些隐式评价呢? 网页方面:页面点击、停留时间、重复访问次数、引用率、Hulu上观看视频的次数; 音乐播放器:播放的曲目、跳过的曲目、播放次数; 这些只是一小部分! 值得注意的是,我们在第二章中学习的算法对于显式评价和隐式评价都是适用的。 本文档使用 书栈(BookStack.CN) 构建 - 73 -

74.什么会阻碍你成功? 什么会阻碍你成功? 什么会阻碍你成功? 什么会阻碍你成功? 设想你有一个成熟的在线音乐网站,在构建推荐系统时会遇到什么问题呢? 假设你有一百万个用户,每次推荐需要计算一百万个距离数据。 如果我们想在一秒钟里进行多次推荐,那计算量将是巨大的。除非增加服务器的数量,否则系统会变 得越来越慢。 说得专业一点,通过邻域进行计算的推荐系统,延迟会变得越来越严重。 还好,这是有解决办法的。 本文档使用 书栈(BookStack.CN) 构建 - 74 -

75.基于用户/物品的协同过滤 基于用户/物品的协同过滤 基于用户的协同过滤 基于物品的协同过滤 能否举个例子? 基于用户的协同过滤 目前为止我们描述的都是基于用户的协同过滤算法。我们将一个用户和其他所有用户进行对比,找到 相似的人。这种算法有两个弊端: 1. 扩展性 上文已经提到,随着用户数量的增加,其计算量也会增加。这种算法在只有几千个用户的 情况下能够工作得很好,但达到一百万个用户时就会出现瓶颈。 2. 稀疏性 大多数推荐系统中,物品的数量要远大于用户的数量,因此用户仅仅对一小部分物品进行 了评价,这就造成了数据的稀疏性。比如亚马逊有上百万本书,但用户只评论了很少一部分,于 是就很难找到两个相似的用户了。 鉴于以上两个局限性,我们不妨考察一下基于物品的协同过滤算法。 基于物品的协同过滤 假设我们有一种算法可以计算出两件物品之间的相似度,比如Phoenix专辑和Manners很相似。如果 一个用户给Phoenix打了很高的分数,我们就可以向他推荐Manners了。 需要注意这两种算法的区别:基于用户的协同过滤是通过计算用户之间的距离找出最相似的用户,并 将他评价过的物品推荐给目标用户;而基于物品的协同过滤则是找出最相似的物品,再结合用户的评 价来给出推荐结果。 能否举个例子? 我们的音乐站点有m个用户和n个乐队,用户会对乐队做出评价,如下表所示: 本文档使用 书栈(BookStack.CN) 构建 - 75 -

76.基于用户/物品的协同过滤 我们要计算Phoenix和Passion Pit之间的相似度,可以使用蓝色方框中的数据,也就是同时对这 两件商品都有过评价的用户。在基于用户的算法中,我们计算的是行与行之间的相似度,而在基于物 品的算法中,我们计算的是列与列之间的。 基于用户的协同过滤又称为内存型协同过滤,因为我们需要将所有的评价数据都保存在内存中来进行推荐。 基于物品的协同过滤也称为基于模型的协同过滤,因为我们不需要保存所有的评价数据,而是通过构建一个物品相似度模型来做推 荐。 本文档使用 书栈(BookStack.CN) 构建 - 76 -

77.修正的余弦相似度 修正的余弦相似度 修正的余弦相似度 计算修正余弦相似度的Python代码 修正的余弦相似度 我们使用余弦相似度来计算两个物品的距离。我们在第二章中提过“分数膨胀”现象,因此我们会从用 户的评价中减去他所有评价的均值,这就是修正的余弦相似度。 左:我喜欢Phoenix乐队,因此给他们打了5分。我不喜欢Passion,所以给了3分。 右:Phoenix很棒,我给4分。Passion Pit太糟糕了,必须给0分! 本文档使用 书栈(BookStack.CN) 构建 - 77 -

78.修正的余弦相似度 U表示同时评价过物品i和j的用户集合 这个公式来自于一篇影响深远的论文《基于物品的协同过滤算法》,由Badrul Sarwar等人合著。 上式表示将用户u对物品i的评价值减去用户u对所有物品的评价均值,从而得到修正后的评分。 s(i,j)表示物品i和j的相似度,分子表示将同时评价过物品i和j的用户的修正评分相乘并求和,分 母则是对所有的物品的修正评分做一些汇总处理。 为了更好地演示修正的余弦相似度,我们举一个例子。下表是五个学生对五位歌手的评价: 首先,我们计算出每个用户的平均评分,这很简单: 下面,我们计算歌手之间的相似度,从Kacey Musgraves和Imagine Dragons开始。上图中我已 本文档使用 书栈(BookStack.CN) 构建 - 78 -

79.修正的余弦相似度 经标出了同时评价过这两个歌手的用户,代入到公式中: 所以这两个歌手之间的修正余弦相似度为0.5260,我计算了其他一些歌手之间的相似度,其余的请读 者们完成: 计算修正余弦相似度的Python代码 1. # -*- coding: utf-8 -*- 本文档使用 书栈(BookStack.CN) 构建 - 79 -

80.修正的余弦相似度 2. 3. from math import sqrt 4. 5. users3 = {"David": {"Imagine Dragons": 3, "Daft Punk": 5, 6. "Lorde": 4, "Fall Out Boy": 1}, 7. "Matt": {"Imagine Dragons": 3, "Daft Punk": 4, 8. "Lorde": 4, "Fall Out Boy": 1}, 9. "Ben": {"Kacey Musgraves": 4, "Imagine Dragons": 3, 10. "Lorde": 3, "Fall Out Boy": 1}, 11. "Chris": {"Kacey Musgraves": 4, "Imagine Dragons": 4, 12. "Daft Punk": 4, "Lorde": 3, "Fall Out Boy": 1}, 13. "Tori": {"Kacey Musgraves": 5, "Imagine Dragons": 4, 14. "Daft Punk": 5, "Fall Out Boy": 3}} 15. 16. 17. def computeSimilarity(band1, band2, userRatings): 18. averages = {} 19. for (key, ratings) in userRatings.items(): 20. averages[key] = (float(sum(ratings.values())) / len(ratings.values())) 21. 22. num = 0 # 分子 23. dem1 = 0 # 分母的第一部分 24. dem2 = 0 25. for (user, ratings) in userRatings.items(): 26. if band1 in ratings and band2 in ratings: 27. avg = averages[user] 28. num += (ratings[band1] - avg) * (ratings[band2] - avg) 29. dem1 += (ratings[band1] - avg) ** 2 30. dem2 += (ratings[band2] - avg) ** 2 31. return num / (sqrt(dem1) * sqrt(dem2)) 32. 33. print(computeSimilarity('Kacey Musgraves', 'Lorde', users3)) 34. print(computeSimilarity('Imagine Dragons', 'Lorde', users3)) 35. print(computeSimilarity('Daft Punk', 'Lorde', users3)) 本文档使用 书栈(BookStack.CN) 构建 - 80 -

81.修正的余弦相似度 这个矩阵看起来不错,那下面该如何使用它来做预测呢?比如我想知道David有多喜欢Kacey Musgraves? p(u,i)表示我们会来预测用户u对物品i的评分,所以p(David, Kacey Musgraves)就表示我们将 预测David会给Kacey打多少分。 N是一个物品的集合,有如下特性: 用户u对集合中的物品打过分 物品i和集合中的物品有相似度数据(即上文中的矩阵) Si,N表示物品i和N的相似度,Ru,N表示用户u对物品N的评分。 为了让公式的计算效果更佳,对物品的评价分值最好介于-1和1之间。 由于我们的评分系统是1至5星,所以需要使用一些运算将其转换到-1至1之间。 本文档使用 书栈(BookStack.CN) 构建 - 81 -

82.修正的余弦相似度 我们的音乐评分系统是5分制,MaxR表示评分系统中的最高分(这里是5),MinR为最低分(这里是 1),Ru,N是用户u对物品N的评分,NRu,N则表示修正后的评分(即范围在-1和1之间)。 若已知NRu,N,求解Ru,N的公式为: 比如一位用户给Fall Out Boy打了2分,那修正后的评分为: 反过来则是: 本文档使用 书栈(BookStack.CN) 构建 - 82 -

83.修正的余弦相似度 有了这个基础后,下面就让我们看看如何求解上文中的p(David, Kacey Musgraves)。 首先我们要修正David对各个物品的评分: 然后结合物品相似度矩阵,代入公式: 所以,我们预测出David对Kacey Musgraves的评分是0.753,将其转换到5星评价体系中: 本文档使用 书栈(BookStack.CN) 构建 - 83 -

84.修正的余弦相似度 最终的预测结果是4.506分。 回顾 修正的余弦相似度是一种基于模型的协同过滤算法。我们前面提过,这种算法的优势之一是扩展 性好,对于大数据量而言,运算速度快、占用内存少。 用户的评价标准是不同的,比如喜欢一个歌手时有些人会打4分,有些打5分;不喜欢时有人会打 3分,有些则会只给1分。修正的余弦相似度计算时会将用户对物品的评分减去用户所有评分的均 值,从而解决这个问题。 本文档使用 书栈(BookStack.CN) 构建 - 84 -

85.Slope One算法 Slope One算法 Slope One算法 第一步:计算差值 第二步:使用加权的Slope One算法进行预测 Slope One算法 还有一种比较流行的基于物品的协同过滤算法,名为Slope One,它最大的优势是简单,因此易于实 现。 Slope One算法是在一篇名为《Slope One:基于在线评分系统的协同过滤算法》的论文中提出 的,由Lemire和Machlachlan合著。这篇论文非常值得一读。 我们用一个简单的例子来了解这个算法。假设Amy给PSY打了3分,Whitney Houston打了4分;Ben 给PSY打了4分。我们要预测Ben会给Whitney Houston打几分。 用表格来描述这个问题即: 我们可以用以下逻辑来预测Ben对Whitney Houston的评分:由于Amy给Whitney Houston打的分 数要比PSY的高一分,所以我们预测Ben也会给高一分,即给到5分。 其实还有其他形式的Slope One算法,比如加权的Slope One。 我们说过Slope One的优势之一是简单,下面说的加权的Slope One看起来会有一些复杂,但是只 要耐心地看下去,事情就会变得很清晰了。 你可以将Slope One分为两个步骤: 首先需要计算出两两物品之间的差值(可以在夜间批量计算)。在上文的例子中,这个步骤就是得出 Whitney Houston要比PSY高一分。 第二步则是进行预测,比如一个新用户Ben来到了我们网站,他从未听过Whitney Houston的歌 曲,我们想要预测他是否喜欢这位歌手。 本文档使用 书栈(BookStack.CN) 构建 - 85 -

86.Slope One算法 通过利用他评价过的歌手以及我们计算好的歌手之间的评分差值,就可以进行预测了。 第一步:计算差值 我们先为上述例子增加一些数据: 计算物品之间差异的公式是: 其中,card(S)表示S中有多少个元素;X表示所有评分值的集合;card(Sj,i(X))则表示同时评价 过物品j和i的用户数。 我们来考察PSY和Taylor Swift之间的差值,card(Sj,i(X))的值是2——因为有两个用户(Amy和 Ben)同时对PSY和Taylor Swift打过分。 分子uj-ui表示用户对j的评分减去对i的评分,代入公式得: 所以PSY和Taylor Swift的差异是2,即用户们给Taylor Swift的评分比PSY要平均高出两分。那 Taylor Swift和PSY的差异呢? 本文档使用 书栈(BookStack.CN) 构建 - 86 -

87.Slope One算法 作业:计算其他物品之间的差值 头脑风暴 试想我们的音乐站点有100万个用户对20万个歌手做评价。如果有一个新进的用户对10个歌手做了评 价,我们是否需要重新计算20万×20万的差异数据,或是有其他更简单的方法? 本文档使用 书栈(BookStack.CN) 构建 - 87 -

88.Slope One算法 答案是你不需要计算整个数据集,这正是Slope One的美妙之处。对于两个物品,我们只需记录同时 评价过这对物品的用户数就可以了。 比如说Taylor Swift和PSY的差值是2,是根据9位用户的评价计算的。当有一个新用户对Taylor Swift打了5分,PSY打了1分时,更新后的差值为: 第二步:使用加权的Slope One算法进行预测 好,现在我们有了物品之间的差异值,下面就用它来进行预测。这里我们将使用加权的Slope One算 法来进行预测,用PWS1来表示,公式为: 其中: PWS1(u)j表示我们将预测用户u对物品i的评分。比如PWS1(Ben)Whitney Houston表示Ben对 Whitney Houston的预测评分。下面就让我们来求解这个问题。 首先来看分子: 表示遍历Ben评价过的所有歌手,除了Whitney Houston以外(也就是-{j}的意思)。 本文档使用 书栈(BookStack.CN) 构建 - 88 -

89. Slope One算法 整个分子的意思是:对于Ben评价过的所有歌手(Whitney Houston除外),找出Whitney Houston和这些歌手之间的差值,并将差值加上Ben对这个歌手的评分。 同时,我们要将这个结果乘以同时评价过两位歌手的用户数。 让我们分解开来看,先将Ben的评分情况和两两歌手之间的差异值展示如下: 1. Ben对Taylor Swift打了5分,也就是ui 2. Whitney Houston和Taylor Swift的差异是-1,即devj,i 3. devj,i + ui = 4 4. 共有两个用户(Amy和Daisy)同时对Taylor Swift和Whitney Houston做了评价,即cj,i = 2 5. 那么(devj,i + ui) cj,i = 4 × 2 = 8 6. Ben对PSY打了2分 7. Whitney Houston和PSY的差异是0.75 8. devj,i + ui = 2.75 9. 有两个用户同时评价了这两位歌手,因此(devj,i + ui) cj,i = 2.75 × 2 = 5.5 10. 分子:8 + 5.5 = 13.5 11. 分母:2 + 2 = 4 12. 预测评分:13.5 ÷ 4 = 3.375 本文档使用 书栈(BookStack.CN) 构建 - 89 -

90.Slope One算法 本文档使用 书栈(BookStack.CN) 构建 - 90 -

91.使用Python实现Slope One算法 使用Python实现Slope One算法 使用Python实现Slope One算法 第一步 第二步 第三步 第四步 加权的Slope One算法:推荐逻辑的实现 MovieLens数据集 使用Python实现Slope One算法 我们将沿用第二章中编写的Python类,重复的代码我不在这里赘述。输入的数据是这样的: 1. users2 = {"Amy": {"Taylor Swift": 4, "PSY": 3, "Whitney Houston": 4}, 2. "Ben": {"Taylor Swift": 5, "PSY": 2}, 3. "Clara": {"PSY": 3.5, "Whitney Houston": 4}, 4. "Daisy": {"Taylor Swift": 5, "Whitney Houston": 3}} 我们先来计算两两物品之间的差异,公式是: 计算后的输出结果应该如下表所示: 括号中的数值表示同时给这两个歌手评过分的用户数。 第一步 1. def computeDeviations(self): 本文档使用 书栈(BookStack.CN) 构建 - 91 -

92.使用Python实现Slope One算法 2. # 获取每位用户的评分数据 3. for ratings in self.data.values(): self.data是一个Python字典,它的values()方法可以获取所有键的值。比如上述代码在第一次迭 代时,ratings变量的值为{“Taylor Swift”: 4, “PSY”: 3, “Whitney Houston”: 4}。 第二步 1. def computeDeviations(self): 2. # 获取每位用户的评分数据 3. for ratings in self.data.values(): 4. # 对于该用户的每个评分项(歌手、分数) 5. for (item, rating) in ratings.items(): 6. self.frequencies.setdefault(item, {}) 7. self.deviations.setdefault(item, {}) 在这个类的初始化方法中,我们需要对self.frequencies和self.deviations进行赋值: 1. def __init__(self, data, k=1, metric='pearson', n=5): 2. ... 3. # 以下变量将用于Slope One算法 4. self.frequencies = {} 5. self.deviations = {} Python字典的setdefault()方法接受两个参数,它的作用是:如果字典中不包含指定的键,则将 其设为默认值;若存在,则返回其对应的值。 第三步 1. def computeDeviations(self): 2. # 获取每位用户的评分数据 3. for ratings in self.data.values(): 4. # 对于该用户的每个评分项(歌手、分数) 5. for (item, rating) in ratings.items(): 6. self.frequencies.setdefault(item, {}) 7. self.deviations.setdefault(item, {}) 8. # 再次遍历该用户的每个评分项 9. for (item2, rating2) in ratings.items(): 10. if item != item2: 11. # 将评分的差异保存到变量中 12. self.frequencies[item].setdefault(item2, 0) 13. self.deviations[item].setdefault(item2, 0.0) 14. self.frequencies[item][item2] += 1 15. self.deviations[item][item2] += rating - rating2 本文档使用 书栈(BookStack.CN) 构建 - 92 -

93.使用Python实现Slope One算法 还是用{“Taylor Swift”: 4, “PSY”: 3, “Whitney Houston”: 4}举例,在第一次遍历中, 外层循环item = “Taylor Swift”,rating = 4;内层循环item2 = “PSY”,rating2 = 3,因此最后一行代码是对self.deviations[“Taylor Swift”][“PSY”]做+1的操作。 第四步 最后,我们便可计算出差异值: 1. def computeDeviations(self): 2. # 获取每位用户的评分数据 3. for ratings in self.data.values(): 4. # 对于该用户的每个评分项(歌手、分数) 5. for (item, rating) in ratings.items(): 6. self.frequencies.setdefault(item, {}) 7. self.deviations.setdefault(item, {}) 8. # 再次遍历该用户的每个评分项 9. for (item2, rating2) in ratings.items(): 10. if item != item2: 11. # 将评分的差异保存到变量中 12. self.frequencies[item].setdefault(item2, 0) 13. self.deviations[item].setdefault(item2, 0.0) 14. self.frequencies[item][item2] += 1 15. self.deviations[item][item2] += rating - rating2 16. 17. for (item, ratings) in self.deviations.items(): 18. for item2 in ratings: 19. ratings[item2] /= self.frequencies[item][item2] 完成了!仅仅用了18代码我们就实现了这个公式: 让我们测试一下: 1. >>> r = recommender(users2) 2. >>> r.computeDeviations() 3. >>> r.deviations 4. {'PSY': {'Taylor Swift': -2.0, 'Whitney Houston': -0.75}, 'Taylor Swift': {'PSY': 2.0, 'Whitney Houston': 1.0}, 'Whitney Houston': {'PSY': 0.75, 'Taylor Swift': -1.0}} 结果和我们之前手工计算的一致: 本文档使用 书栈(BookStack.CN) 构建 - 93 -

94.使用Python实现Slope One算法 感谢Bryan O’Sullivan,这里用Python实现的Slope One算法正是基于他的成果。 加权的Slope One算法:推荐逻辑的实现 1. def slopeOneRecommendations(self, userRatings): 2. recommendations = {} 3. frequencies = {} 4. # 遍历目标用户的评分项(歌手、分数) 5. for (userItem, userRating) in userRatings.items(): 6. # 对目标用户未评价的歌手进行计算 7. for (diffItem, diffRatings) in self.deviations.items(): 本文档使用 书栈(BookStack.CN) 构建 - 94 -

95.使用Python实现Slope One算法 8. if diffItem not in userRatings and userItem in self.deviations[diffItem]: 9. freq = self.frequencies[diffItem][userItem] 10. recommendations.setdefault(diffItem, 0.0) 11. frequencies.setdefault(diffItem, 0) 12. # 分子 13. recommendations[diffItem] += (diffRatings[userItem] + userRating) * freq 14. # 分母 15. frequencies[diffItem] += freq 16. 17. recommendations = [(k, v / frequencies[k]) for (k, v) in recommendations.items()] 18. # 排序并返回 19. recommendations.sort(key=lambda artistTuple: artistTuple[1], reverse=True) 20. return recommendations 21. 22. >>> r.slopeOneRecommendations(users2['Ben']) 23. [('Whitney Houston', 3.375)] MovieLens数据集 让我们在另一个数据集上尝试一下Slope One算法。 MovieLens数据集是由明尼苏达州大学的GroupLens研究项目收集的,是用户对电影的评分。这个 数据集可以在www.grouplens.org下载,有三种大小,这里我使用的是最小的那个,包含了943位 用户对1682部电影的评价,约10万条记录。 我们一起来测试一下: 1. >>> r = recommender(0) 2. >>> r.loadMovieLens('/Users/raz/Downloads/ml-100k/') 3. 102625 4. >>> r.computeDeviations() # 大约需要50秒 5. >>> r.slopeOneRecommendations(r.data['25']) 6. [('Aiqing wansui (1994)', 5.674418604651163), ('Boys, Les (1997)', 5.523076923076923), ...] 作业 1. 看看Slope One的推荐结果是否靠谱:对数据集中的10部电影进行评分,得到的推荐结果是否是 你喜欢的电影呢? 2. 实现修正的余弦相似度算法,比较一下两者的运算效率。 3. (较难)我的笔记本电脑有8G内存,在尝试用Slope One计算图书漂流站数据集时报内存溢出 了。那个数据集中有27万本书,因此需要保存超过7300万条记录的Python字典。这个字典的数 据是否很稀疏呢?修改算法,让它能够处理更多数据吧。 本文档使用 书栈(BookStack.CN) 构建 - 95 -

96.使用Python实现Slope One算法 祝贺大家学完第三章了 本文档使用 书栈(BookStack.CN) 构建 - 96 -

97.分类 分类 第四章:分类 第四章:分类 原文:http://guidetodatamining.com/chapter4 在上几章中我们使用用户对物品的评价来进行推荐,这一章我们将使用物品本身的特征来进行推荐。 这也是潘多拉音乐站所使用的方法。 内容: 潘多拉推荐系统简介 特征值选择的重要性 示例:音乐特征值和邻域算法 数据标准化 修正的标准分数 Python代码:音乐,特征,以及简单的邻域算法实现 一个和体育相关的示例 特征值抽取方式一览 数据集: athletesTrainingSet.txt athletesTestSet.txt irisTrainingSet.data irisTestSet.data mpgTrainingSet.txt mpgTestSet.txt 本文档使用 书栈(BookStack.CN) 构建 - 97 -

98.根据物品特征进行分类 根据物品特征进行分类 根据物品特征进行分类 特征值选取的重要性 一个简单的示例 使用Python实现推荐逻辑 如何显示“推荐理由”? 评分标准的问题 标准化 标准分带来的问题 修正的标准分 是否需要标准化? 根据物品特征进行分类 前几章我们讨论了如何使用协同过滤来进行推荐,由于使用的是用户产生的各种数据,因此又称为社 会化过滤算法。 比如你购买了Phoenix专辑,我们网站上其他购买过这张专辑的用户还会去购买Vampire的专辑,因 此会把它推荐给你;我在Netflix上观看了Doctor Who,网站会向我推荐Quantum Leap,用的是 同样的原理。 我们同时也讨论了协同过滤会遇到的种种问题,包括数据的稀疏性和算法的可扩展性。此外,协同过 滤算法倾向于推荐那些已经很流行的物品。 试想一个极端的例子:一个新乐队发布了专辑,这张专辑还没有被任何用户评价或购买过,那它将永 远不会出现在推荐列表中。 这类推荐系统会让流行的物品更为流行,冷门的物品更无人问津。 — Daniel Fleder & Kartik Hosanagar 2009 《推荐系统对商品分类的影响》 这一章我们来看另一种推荐方法。 以潘多拉音乐站举例,在这个站点上你可以设立各种音乐频道,只需为这个频道添加一个歌手,潘多 拉就会播放和这个歌手风格相类似的歌曲。 比如我添加了Phoenix乐队,潘多拉便会播放El Ten Eleven的歌曲。它并没有使用协同过滤,而 是通过计算得到这两个歌手的音乐风格是相似的。 其实在播放界面上可以看到推荐理由: 本文档使用 书栈(BookStack.CN) 构建 - 98 -

99.根据物品特征进行分类 “根据你目前告知的信息,我们播放的这首歌曲有着相似的旋律,使用了声响和电音的组合,即兴的吉 他伴奏。” 在我的Hiromi音乐站上,潘多拉会播放E.S.T.的歌曲,因为“它有着古典爵士乐风,一段高水准的 钢琴独奏,轻盈的打击乐,以及有趣的歌曲结构。” 潘多拉网站的推荐系统是基于一个名为音乐基因的项目。 他们雇佣了专业的音乐家对歌曲进行分类(提取它们的“基因”)。这些音乐家会接受超过150小时的 训练,之后便可用20到30分钟的时间来分析一首歌曲。 本文档使用 书栈(BookStack.CN) 构建 - 99 -

100.根据物品特征进行分类 这些乐曲特征是很专业的: 这些专家要甄别400多种特征,平均每个月会有15000首新歌曲,因此这是一项非常消耗人力的工 程。 注意:潘多拉的音乐基因项目是商业机密,我不曾了解它的任何信息。下文讲述的是如何构造一个类似的系统。 特征值选取的重要性 假设潘多拉会用曲风和情绪作为歌曲特征,分值如下: 曲风:乡村1分,爵士2分,摇滚3分,圣歌4分,饶舌5分 情绪:悲伤的1分,欢快的2分,热情的3分,愤怒的4分,不确定的5分 比如James Blunt的那首You’re Beautiful是悲伤的摇滚乐,用图表来展示它的位置便是: 本文档使用 书栈(BookStack.CN) 构建 - 100 -

101.根据物品特征进行分类 比如一个叫Tex的用户喜欢You’re Beautiful这首歌,我们想要为他推荐歌曲。 我们的歌曲库中有另外三首歌:歌曲1是悲伤的爵士乐;歌曲2是愤怒的圣歌;歌曲3是愤怒的摇滚 乐。 本文档使用 书栈(BookStack.CN) 构建 - 101 -

102.根据物品特征进行分类 你会推荐哪一首? 图中歌曲1看起来是最相近的。 也许你已经看出了这种算法中的不足,因为不管用何种计算距离的公式,爵士乐和摇滚乐是相近的, 悲伤的乐曲和快乐的乐曲是相近的等等。 即使调整了分值的分配,也不能解决问题。这就是没有选取好特征值的例子。 不过解决的方法也很简单,我们将每种歌曲类型拆分成单独的特征,并对此进行打分: “乡村音乐”一栏的1分表示完全不是这个乐曲风格,5分则表示很相符。 这样一来,评分值就显得有意义了。如果一首歌的“乡村音乐”特征是4分,另一首是5分,那我们可以 本文档使用 书栈(BookStack.CN) 构建 - 102 -

103.根据物品特征进行分类 认为它们是相似的歌曲。 其实这就是潘多拉所使用的特征抽取方法。每个特征都是1到5分的尺度,0.5分为一档。特征会被分 到不同的大类中。 通过这种方式,潘多拉将每首歌曲都抽象成一个包含400个数值元素的向量,并结合我们之前学过的 距离计算公式进行推荐。 一个简单的示例 我们先来构建一个数据集,我选取了以下这些特征(可能比较随意),使用5分制来评分(0.5分一 档): 使用钢琴的程度(Piano):1分表示没有使用钢琴,5分表示整首歌曲由钢琴曲贯穿; 使用美声的程度(Vocals):标准同上 节奏(Driving beat):整首歌曲是否有强烈的节奏感 蓝调(Blues infl.) 电音吉他(Dirty elec. Guitar) 幕后和声(Backup vocals) 饶舌(Rap infl.) 使用以上标准对一些歌曲进行评分: 本文档使用 书栈(BookStack.CN) 构建 - 103 -

104.根据物品特征进行分类 然后我们便可以使用距离计算公式了,比如要计算Dr. Dog的Fate歌曲和Phoenix的Lisztomania 之间的曼哈顿距离: 相加得到两首歌曲的曼哈顿距离为9。 使用Python实现推荐逻辑 回忆一下,我们在协同过滤中使用的用户评价数据是这样的: 1. users = {"Angelica": {"Blues Traveler": 3.5, "Broken Bells": 2.0, "Norah Jones": 4.5, "Phoenix": 5.0, "Slightly Stoopid": 1.5, "The Strokes": 2.5, "Vampire Weekend": 2.0}, 2. "Bill":{"Blues Traveler": 2.0, "Broken Bells": 3.5, "Deadmau5": 4.0, "Phoenix": 2.0, "Slightly Stoopid": 3.5, "Vampire Weekend": 3.0}} 我们将上文中的歌曲特征数据也用类似的格式储存起来: 本文档使用 书栈(BookStack.CN) 构建 - 104 -

105.根据物品特征进行分类 1. music = {"Dr Dog/Fate": {"piano": 2.5, "vocals": 4, "beat": 3.5, "blues": 3, "guitar": 5, "backup vocals": 4, "rap": 1}, 2. "Phoenix/Lisztomania": {"piano": 2, "vocals": 5, "beat": 5, "blues": 3, "guitar": 2, "backup vocals": 1, "rap": 1}, 3. "Heartless Bastards/Out at Sea": {"piano": 1, "vocals": 5, "beat": 4, "blues": 2, "guitar": 4, "backup vocals": 1, "rap": 1}, 4. "Todd Snider/Don't Tempt Me": {"piano": 4, "vocals": 5, "beat": 4, "blues": 4, "guitar": 1, "backup vocals": 5, "rap": 1}, 5. "The Black Keys/Magic Potion": {"piano": 1, "vocals": 4, "beat": 5, "blues": 3.5, "guitar": 5, "backup vocals": 1, "rap": 1}, 6. "Glee Cast/Jessie's Girl": {"piano": 1, "vocals": 5, "beat": 3.5, "blues": 3, "guitar":4, "backup vocals": 5, "rap": 1}, 7. "La Roux/Bulletproof": {"piano": 5, "vocals": 5, "beat": 4, "blues": 2, "guitar": 1, "backup vocals": 1, "rap": 1}, 8. "Mike Posner": {"piano": 2.5, "vocals": 4, "beat": 4, "blues": 1, "guitar": 1, "backup vocals": 1, "rap": 1}, 9. "Black Eyed Peas/Rock That Body": {"piano": 2, "vocals": 5, "beat": 5, "blues": 1, "guitar": 2, "backup vocals": 2, "rap": 4}, 10. "Lady Gaga/Alejandro": {"piano": 1, "vocals": 5, "beat": 3, "blues": 2, "guitar": 1, "backup vocals": 2, "rap": 1}} 假设我有一个朋友喜欢Black Keys Magic Potion,我便可根据曼哈顿距离来进行推荐: 1. >>> computeNearestNeighbor('The Black Keys/Magic Potion', music) 2. [(4.5, 'Heartless Bastards/Out at Sea'), (5.5, 'Phoenix/Lisztomania'), (6.5, 'Dr Dog/Fate'), (8.0, "Glee Cast/Jessie's Girl"), (9.0, 'Mike Posner'), (9.5, 'Lady Gaga/Alejandro'), (11.5, 'Black Eyed Peas/Rock That Body'), (11.5, 'La Roux/Bulletproof'), (13.5, "Todd Snider/Don't Tempt Me")] 这里我推荐的是Heartless Bastard的Out as Sea,还是很合乎逻辑的。 当然,由于我们的数据集比较小,特征和歌曲都不够丰富,因此有些推荐结果并不太好。 这段代码可以点此浏览。 如何显示“推荐理由”? 潘多拉在推荐歌曲时会显示推荐理由,我们也可以做到这一点。 比如在上面的例子中,我们可以将Magic Potion和Out at Sea的音乐特征做一个比较,找出高度 相符的点: 本文档使用 书栈(BookStack.CN) 构建 - 105 -

106.根据物品特征进行分类 可以看到,两首歌曲最相似的地方是钢琴、和声、以及饶舌,这些特征的差异都是0。但是,这些特征 的评分都很低,我们不能告诉用户“因为这首歌曲没有钢琴伴奏,所以我们推荐给你”。 因此,我们需要使用那些相似的且评分较高的特征。 我们推荐歌曲是因为它有着强烈的节奏感,美声片段,以及电音吉他的演奏。 评分标准的问题 假如我想增加一种音乐特征——每分钟的鼓点数(bpm),用来判断这是一首快歌还是慢歌。 以下是扩充后的数据集: 本文档使用 书栈(BookStack.CN) 构建 - 106 -

107.根据物品特征进行分类 没有bpm时,Magic Potion和Out at Sea距离最近,和Smells Like Teen Spirit距离最远。 但引入bpm后,我们的结果就乱套了,因为bpm基本上就决定了两首歌的距离。现在Bad Plus和The Black Keys距离最近就是因为bpm数据相近。 再举个有趣的例子。在婚恋网站上,我通过用户的年龄和收入来进行匹配: 这样一来,年龄的最大差异是28,而薪资的最大差异则是72,000。因为差距悬殊,薪水的高低基本 决定了匹配程度。 如果单单目测,我们会将David推荐给Yun,因为他们年龄相近,工资也差不多。 本文档使用 书栈(BookStack.CN) 构建 - 107 -

108.根据物品特征进行分类 但如果使用距离计算公式,那么53岁的Brian就会被匹配给Yun,这就不太妙了。 事实上,评分标准不一是所有推荐系统的大敌! 标准化 不用担心,我们可以使用标准化。 要让数据变得可用我们可以对其进行标准化,最常用的方法是将所有数据都转化为0到1之间的值。 本文档使用 书栈(BookStack.CN) 构建 - 108 -

109.根据物品特征进行分类 拿上面的薪酬数据举例,最大值115,000和最小值43,000相差72,000,要让所有值落到0到1之间, 可以将每个值减去最小值,并除以范围(72,000)。 所以,Yun标准化之后的薪水是: 对一些数据集,这种简单的方法效果是不错的。 如果你学过统计学,会知道还有其他的标准化方法。 比如说标准分(z-score)——分值偏离均值的程度: 标准差的计算公式是: 本文档使用 书栈(BookStack.CN) 构建 - 109 -

110.根据物品特征进行分类 card(x)表示集合x中的元素个数。 如果你对统计学有兴趣,可以读一读《漫话统计学》。 我们用上文中交友网站的数据举例。所有人薪水的总和是577,000,一共有8人,所以均值为 72,125。 代入标准差的计算公式: 那Yun的标准分则是: 练习题:计算Allie、Daniela、Rita的标准分 本文档使用 书栈(BookStack.CN) 构建 - 110 -

111.根据物品特征进行分类 标准分带来的问题 标准分的问题在于它会受异常值的影响。 比如说一家公司有100名员工,普通员工每小时赚10美元,而CEO一年能赚600万,那全公司的平均时 薪为: 结果是每小时38美元,看起来很美好,但其实并不真实。鉴于这个原因,标准分的计算公式会稍作变 化。 修正的标准分 计算方法:将标准分公式中的均值改为中位数,将标准差改为绝对偏差。 以下是绝对偏差的计算公式: 本文档使用 书栈(BookStack.CN) 构建 - 111 -

112.根据物品特征进行分类 中位数指的是将所有数据进行排序,取中间的那个值。如果数据量是偶数,则取中间两个数值的均 值。 下面就让我们试试吧。 首先将所有人按薪水排序,找到中位数,然后计算绝对偏差: 本文档使用 书栈(BookStack.CN) 构建 - 112 -

113.根据物品特征进行分类 最后,我们便可以计算得出Yun的修正标准分: 是否需要标准化? 当物品的特征数值尺度不一时,就有必要进行标准化。 比如上文中音乐特征里大部分是1到5分,鼓点数却是60到180;交友网站中薪水和年龄这两个尺度也 有很大差别。 再比如我想在新墨西哥圣达菲买一处宅子,下表是一些选择: 本文档使用 书栈(BookStack.CN) 构建 - 113 -

114.根据物品特征进行分类 可以看到,价格的范围是最广的,在计算距离时会起到决定性作用;同样,有两间卧室和有二十间卧 室,在距离的影响下作用也会很小。 需要进行标准化的情形: 1. 我们需要通过物品特性来计算距离; 2. 不同特性之间的尺度相差很大。 但对于那种“赞一下”、“踩一脚”的评分数据,就没有必要做标准化了: 在潘多拉的例子中,如果所有的音乐特征都是在1到5分之间浮动的,是否还需要标准化呢? 虽然即使做了也不会影响计算结果,但是任何运算都是有性能消耗的,这时我们可以通过比较两种方 式的性能和效果来做进一步选择。 在下文中,我们会看到标准化反而会降低结果正确性的示例。 本文档使用 书栈(BookStack.CN) 构建 - 114 -

115.回到潘多拉 回到潘多拉 回到潘多拉 使用Python实现最邻近分类算法 回到潘多拉 在潘多拉网站的示例中,我们用一个特征向量来表示一首歌曲,用以计算歌曲的相似度。 潘多拉网站同样允许用户对歌曲“赞”和“踩”,那我们要如何利用这些数据呢? 假设我们的歌曲有两个特征,重金属吉他(Dirty Guitar)和强烈的节奏感(Driving Beat), 两种特征都在1到5分之间。 一位用户对5首歌曲做了“赞”的操作(图中的L),另外五首则“踩”了一下(图中的D): 图中多了一个问号所表示的歌曲,你觉得用户会喜欢它还是不喜欢呢? 想必你也猜到了,因为这个问号离用户喜欢的歌曲距离较近。这一章接下来的篇幅都会用来讲述这种 计算方法。 最明显的方式是找到问号歌曲最邻近的歌曲,因为它们之间相似度比较高,再根据用户是否喜欢这些 邻近歌曲来判断他对问号歌曲的喜好。 使用Python实现最邻近分类算法 我们仍使用上文中的歌曲示例,用7个特征来标识10首歌曲: 本文档使用 书栈(BookStack.CN) 构建 - 115 -

116.回到潘多拉 使用Python代码来表示这些数据: 1. music = {"Dr Dog/Fate": {"piano": 2.5, "vocals": 4, "beat": 3.5, "blues": 3, "guitar": 5, "backup vocals": 4, "rap": 1}, 2. "Phoenix/Lisztomania": {"piano": 2, "vocals": 5, "beat": 5, "blues": 3, "guitar": 2, "backup vocals": 1, "rap": 1}, 3. "Heartless Bastards/Out at Sea": {"piano": 1, "vocals": 5, "beat": 4, "blues": 2, "guitar": 4, "backup vocals": 1, "rap": 1}, 4. "Todd Snider/Don't Tempt Me": {"piano": 4, "vocals": 5, "beat": 4, "blues": 4, "guitar": 1, "backup vocals": 5, "rap": 1}, 5. "The Black Keys/Magic Potion": {"piano": 1, "vocals": 4, "beat": 5, "blues": 3.5, "guitar": 5, "backup vocals": 1, "rap": 1}, 6. "Glee Cast/Jessie's Girl": {"piano": 1, "vocals": 5, "beat": 3.5, "blues": 3, "guitar":4, "backup vocals": 5, "rap": 1}, 7. "La Roux/Bulletproof": {"piano": 5, "vocals": 5, "beat": 4, "blues": 2, "guitar": 1, "backup vocals": 1, "rap": 1}, 8. "Mike Posner": {"piano": 2.5, "vocals": 4, "beat": 4, "blues": 1, "guitar": 1, "backup vocals": 1, "rap": 1}, 9. "Black Eyed Peas/Rock That Body": {"piano": 2, "vocals": 5, "beat": 5, "blues": 1, "guitar": 2, "backup vocals": 2, "rap": 4}, 10. "Lady Gaga/Alejandro": {"piano": 1, "vocals": 5, "beat": 3, "blues": 2, "guitar": 1, "backup vocals": 2, "rap": 1}} 这样做虽然可行,但却比较繁琐,piano、vocals这样的键名需要重复很多次。 本文档使用 书栈(BookStack.CN) 构建 - 116 -

117.回到潘多拉 我们可以将其简化为向量,即Python中的数组类型: 1. # 2. # 物品向量中的特征依次为:piano, vocals, beat, blues, guitar, backup vocals, rap 3. # 4. items = {"Dr Dog/Fate": [2.5, 4, 3.5, 3, 5, 4, 1], 5. "Phoenix/Lisztomania": [2, 5, 5, 3, 2, 1, 1], 6. "Heartless Bastards/Out": [1, 5, 4, 2, 4, 1, 1], 7. "Todd Snider/Don't Tempt Me": [4, 5, 4, 4, 1, 5, 1], 8. "The Black Keys/Magic Potion": [1, 4, 5, 3.5, 5, 1, 1], 9. "Glee Cast/Jessie's Girl": [1, 5, 3.5, 3, 4, 5, 1], 10. "La Roux/Bulletproof": [5, 5, 4, 2, 1, 1, 1], 11. "Mike Posner": [2.5, 4, 4, 1, 1, 1, 1], 12. "Black Eyed Peas/Rock That Body": [2, 5, 5, 1, 2, 2, 4], 13. "Lady Gaga/Alejandro": [1, 5, 3, 2, 1, 2, 1]} 本文档使用 书栈(BookStack.CN) 构建 - 117 -

118.回到潘多拉 接下来我还需要将用户“赞”和“踩”的数据也用Python代码表示出来。 由于用户并不会对所有的歌曲都做这些操作,所以我用嵌套的字典来表示: 1. users = {"Angelica": {"Dr Dog/Fate": "L", 2. "Phoenix/Lisztomania": "L", 3. "Heartless Bastards/Out at Sea": "D", 4. "Todd Snider/Don't Tempt Me": "D", 5. "The Black Keys/Magic Potion": "D", 本文档使用 书栈(BookStack.CN) 构建 - 118 -

119.回到潘多拉 6. "Glee Cast/Jessie's Girl": "L", 7. "La Roux/Bulletproof": "D", 8. "Mike Posner": "D", 9. "Black Eyed Peas/Rock That Body": "D", 10. "Lady Gaga/Alejandro": "L"}, 11. "Bill": {"Dr Dog/Fate": "L", 12. "Phoenix/Lisztomania": "L", 13. "Heartless Bastards/Out at Sea": "L", 14. "Todd Snider/Don't Tempt Me": "D", 15. "The Black Keys/Magic Potion": "L", 16. "Glee Cast/Jessie's Girl": "D", 17. "La Roux/Bulletproof": "D", 18. "Mike Posner": "D", 19. "Black Eyed Peas/Rock That Body": "D", 20. "Lady Gaga/Alejandro": "D"}} 这里使用L和D两个字母来表示喜欢和不喜欢,当然你也可以用其他方式,比如0和1等。 对于新的向量格式,我们需要对曼哈顿距离函数和邻近物品函数做一些调整: 1. def manhattan(vector1, vector2): 2. distance = 0 3. total = 0 4. n = len(vector1) 5. for i in range(n): 6. distance += abs(vector1[i] - vector2[i]) 7. return distance 8. 9. def computeNearestNeighbor(itemName, itemVector, items): 10. """按照距离排序,返回邻近物品列表""" 11. distances = [] 12. for otherItem in items: 13. if otherItem != itemName: 14. distance = manhattan(itemVector, items[otherItem]) 15. distances.append((distance, otherItem)) 16. # 最近的排在前面 17. distances.sort() 18. return distances 最后,我需要建立一个分类函数,用来预测用户对一个新物品的喜好,如: 1. "Chris Cagle/I Breathe In. I Breathe Out" [1, 5, 2.5, 1, 1, 5, 1] 这个函数会先计算出与这个物品距离最近的物品,然后找到用户对这个最近物品的评价,以此作为新 物品的预测值。 本文档使用 书栈(BookStack.CN) 构建 - 119 -

120.回到潘多拉 下面是一个最简单的分类函数: 1. def classify(user, itemName, itemVector): 2. nearest = computeNearestNeighbor(itemName, itemVector, items)[0][1] 3. rating = users[user][nearest] 4. return rating 让我们试用一下: 1. >>> classify('Angelica', 'Chris Cagle/I Breathe In. I Breathe Out', [1, 5, 2.5, 1, 1, 5, 1]) 2. 'L' 我们认为她会喜欢这首歌曲!为什么呢? 1. >>> computeNearestNeighbor('Chris Cagle/I Breathe In. I Breathe Out', [1, 5, 2.5, 1, 1, 5, 1], items) 2. [(4.5, 'Lady Gaga/Alejandro'), (6.0, "Glee Cast/Jessie's Girl"), (7.5, "Todd Snider/Don't Tempt Me"), (8.0, 'Mike Posner'), (9.5, 'Heartless Bastards/Out'), (10.5, 'Black Eyed Peas/Rock That Body'), (10.5, 'Dr Dog/Fate'), (10.5, 'La Roux/Bulletproof'), (10.5, 'Phoenix/Lisztomania'), (14.0, 'The Black Keys/Magic Potion')] 可以看到,距离I Breathe In最近的歌曲是Alejandro,并且Angelica是喜欢这首歌曲的,所以 我们预测她也会喜欢I Breathe In。 其实我们做的是一个分类器,将歌曲分为了用户喜欢和不喜欢两个类别。 号外,号外!我们编写了一个分类器! 本文档使用 书栈(BookStack.CN) 构建 - 120 -

121.回到潘多拉 分类器是指通过物品特征来判断它应该属于哪个组或类别的程序! 分类器程序会基于一组已经做过分类的物品进行学习,从而判断新物品的所属类别。 在上面的例子中,我们知道Angelica喜欢和不喜欢的歌曲,然后据此判断她是否会喜欢Chris Cagle的歌。 1. 在Angelica评价过的歌曲中找到距离Chris Cagle最近的歌曲,即Laydy Gaga的 Alejandro; 2. 由于Angelica是喜欢Alejandro这首歌的,所以我们预测她也会喜欢Chris Cagle的 Breathe In, Breathe Out。 分类器的应用范围很广,以下是一些示例: 推特情感分类 很多人在对推特中的文字消息进行情感分类(积极的、消极的),可以有很多用途,如Axe发布了一 款新的腋下除臭剂,通过推文就能知道用户是否满意。这里用到的物品特征是文字信息。 人脸识别 现在有些手机应用可以识别出照片里你的朋友们,这项技术也可用于监控录像中的人脸识别。不同的 识别技术细节可能不同,但都会用到诸如五官的大小和相对距离等信息。 政治拉票 本文档使用 书栈(BookStack.CN) 构建 - 121 -

122.回到潘多拉 通过将目标选民分为“爱凑热闹”、“很有主见”、“家庭为重”等类型,来进行有针对性的拉票活动。 市场细分 这和上个例子有点像,与其花费巨额广告费向不可能购买维加斯公寓的人进行宣传,不如从人群中识 别出潜在客户,缩小宣传范围。最好能再对目标群体进行细分,进一步定制广告内容。 个人健康助理 如今人们越来越关注自身,我们可以购买到像Nike健身手环这样的产品,而Intel等公司也在研制一 种智能家居,可以在你行走时称出你的重量,记录你的行动轨迹,并给出健康提示。 有些专家还预言未来我们会穿戴各种便携式设备,收集我们的生活信息,并加以分类。 其他 识别恐怖分子 来信分类(重要的、一般的、垃圾邮件) 预测医疗费用 识别金融诈骗 本文档使用 书栈(BookStack.CN) 构建 - 122 -

123.她是从事什么运动的? 她是从事什么运动的? 她是从事什么运动的? 答案 测试数据 她是从事什么运动的? 让我们来为之后的几章做一个预热,先看一个较为简单的例子——根据女运动员的身高和体重来判断她 们是从事什么运动项目的。 下表是原始数据: 本文档使用 书栈(BookStack.CN) 构建 - 123 -

124.她是从事什么运动的? 这里列出的是2008和2012奥运会上排名靠前的二十位女运动员。 篮球运动员参加了WNBA;田径运动员则完成了2012年奥运会的马拉松赛。虽然数据量很小,但我们 仍可以对其应用一些数据挖掘算法。 你可以看到上表中列出了运动员的年龄,光凭这一信息就能进行一些预测了。 比如,以下运动员会是哪个项目的呢? 本文档使用 书栈(BookStack.CN) 构建 - 124 -

125.她是从事什么运动的? 答案 Candace Parker是篮球运动员,McKayla Maroney是美国女子体操队的一员,Olivera Jevtic 是塞尔维亚的一名长跑运动员,Lisa Jane Weightman则是澳大利亚的长跑运动员。 看,我们刚刚就进行了一次分类——通过运动员的年龄特征来识别她们参与的体育项目。 头脑风暴 假设我想通过运动员的身高和体重来预测她所从事的运动,数据集只有两人:Nakia Sanford是篮 球运动员,身高6尺4寸(76英寸,1.93米),体重200磅(90公斤);Sarah Beale是橄榄球运动 员,身高5尺10寸(70英寸,1.78米),体重190磅(86公斤)。 本文档使用 书栈(BookStack.CN) 构建 - 125 -

126.她是从事什么运动的? 我想知道Catherine Spencer是从事哪项运动的,她的身高是5尺10寸,重200磅,如何预测呢? 如果你认为她是橄榄球运动员,那么你猜对了。但是,如果用曼哈顿距离来进行计算,Catherine和 Nakia的距离是6,和Sarah的距离是10,那应该预测她是篮球运动员才对。 我们之前是否学过一个方法,能让距离计算更为准确呢? 没错,就是修正的标准分! 测试数据 下表是我们需要进行预测的运动员列表,一起来做分类器吧! 本文档使用 书栈(BookStack.CN) 构建 - 126 -

127.Python编码 Python编码 Python编码 关于断言 答案 答案 答案 鸢尾花数据集 Python编码 这次我们不将数据直接写在Python代码中,而是放到两个文本文件里: athletesTrainingSet.txt和athletesTestSet.txt。 我会使用第一个文件中的数据来训练分类器,然后使用测试文件里的数据来进行评价。 文件格式大致如下: 文件中的每一行是一条完整的记录,字段使用制表符分隔。 我要使用运动员的身高体重数据来预测她所从事的运动项目,也就是用第三、四列的数据来预测第二 列的数据。 运动员的姓名不会使用到,我们既不能通过运动员的姓名得知她参与的项目,也不会通过身高体重来 预测运动员的姓名。 本文档使用 书栈(BookStack.CN) 构建 - 127 -

128.Python编码 你好,你有五英尺高,150磅重,莫非你的名字是Clara Coleman? 当然,名字也有它的用处,我们可以用它来解释分类器的预测结果:“我们认为Amelia Pond是一名 体操运动员,因为她的身高体重和另一名体操运动员Gabby Douglas很接近。” 为了让我们的Python代码更具一般性,并不只适用于这一种数据集,我会为每一列数据增加一个列 名,如: 所有被标记为comment的列都会被分类器忽略;标记为class的列表示物品所属分类;不定个数的 num列则表示物品的特征。 头脑风暴 我们在Python中应该如何表示这些数据呢?以下是一些可能性: 1. # 1 2. {'Asuka Termoto': ('Gymnastics', [54, 66]), 3. 'Brittainey Raven': ('Basketball', [72, 162]), ...} 这种方式使用了运动员的姓名作为键,而我们说过分类器程序根本不会使用到姓名,所以不合理。 1. # 2 2. [['Asuka Termoto', 'Gymnastics', 54, 66], 3. ['Brittainey Raven', 'Basketball', 72, 162], ...] 本文档使用 书栈(BookStack.CN) 构建 - 128 -

129.Python编码 这种方式看起来不错,它直接反映了文件的格式。由于我们需要遍历文件的数据,所以使用列表类型 (list)是合理的。 1. # 3 2. [('Gymnastics', [54, 66], ['Asuka Termoto']), 3. ('Basketball', [72, 162], ['Brittainey Raven']), ...] 这是我最认同的表示方式,因为它将不同类型的数据区别开来了,依次是分类、特征、备注。这里备 注可能有多个,所以也用了一个列表来表示。 以下是读取数据文件并转换成上述格式的函数: 1. class Classifier: 2. 3. def __init__(self, filename): 4. 5. self.medianAndDeviation = [] 6. 7. # 读取文件 8. f = open(filename) 9. lines = f.readlines() 10. f.close() 11. self.format = lines[0].strip().split('\t') 12. self.data = [] 13. for line in lines[1:]: 14. fields = line.strip().split('\t') 15. ignore = [] 16. vector = [] 17. for i in range(len(fields)): 18. if self.format[i] == 'num': 19. vector.append(int(fields[i])) 20. elif self.format[i] == 'comment': 21. ignore.append(fields[i]) 22. elif self.format[i] == 'class': 23. classification = fields[i] 24. self.data.append((classification, vector, ignore)) 动手实践 在计算修正的标准分之前,我们需要编写获取中位数和计算绝对偏差的函数,尝试实现这两个函数: 1. >>> heights = [54, 72, 78, 49, 65, 63, 75, 67, 54] 2. >>> median = classifier.getMedian(heights) 3. >>> median 4. 65 5. >>> asd = classifier.getAbsoluteStandardDeviation(heights, median) 本文档使用 书栈(BookStack.CN) 构建 - 129 -

130.Python编码 6. >>> asd 7. 8.0 关于断言 通常我们会将一个大的算法拆分成几个小的组件,并为每个组件编写一些单元测试,从而确保它能正 常工作。 很多时候,我们会先写单元测试,再写正式的代码。在我提供的模板代码中已经编写了一些单元测试 摘录如下: 1. def unitTest(): 2. list1 = [54, 72, 78, 49, 65, 63, 75, 67, 54] 3. classifier = Classifier('athletesTrainingSet.txt') 4. m1 = classifier.getMedian(list1) 5. assert(round(m1, 3) == 65) 6. ... 7. print("getMedian和getAbsoluteStandardDeviation均能正常工作") 你需要完成的geMedian函数的模板是: 1. def getMedian(self, alist): 2. """返回中位数""" 3. 4. """请在此处编写代码""" 5. return 0 这个模板函数返回的是0,你需要编写代码来返回列表的中位数。 比如单元测试中我传入了以下列表: 1. [54, 72, 78, 49, 65, 63, 75, 67, 54] assert(断言)表示函数的返回值应该是65。如果所有的单元测试都能通过,则报告以下信息: 1. getMedian和getAbsoluteStandardDeviation均能正常工作 否则,则抛出以下异常: 1. File "testMedianAndASD.py", line 78, in unitTest 2. assert(round(m1, 3) == 65) 3. AssertError 本文档使用 书栈(BookStack.CN) 构建 - 130 -

131.Python编码 断言在单元测试中是很常用的。 将大型代码拆分成一个个小的部分,并为每个部分编写单元测试,这一点是很重要的。如果没有单元测试,你将无法知道自己是否正 确完成了所有任务,以及未来的某个修改是否会导致你的程序不可用。—- Peter Norvig 答案 1. def getMedian(self, alist): 2. """返回中位数""" 3. if alist == []: 4. return [] 5. blist = sorted(alist) 6. length = len(alist) 7. if length % 2 == 1: 8. # 列表有奇数个元素,返回中间的元素 9. return blist[int(((length + 1) / 2) - 1)] 10. else: 11. # 列表有偶数个元素,返回中间两个元素的均值 12. v1 = blist[int(length / 2)] 13. v2 = blist[(int(length / 2) - 1)] 14. return (v1 + v2) / 2.0 15. 16. def getAbsoluteStandardDeviation(self, alist, median): 17. """计算绝对偏差""" 18. sum = 0 19. for item in alist: 本文档使用 书栈(BookStack.CN) 构建 - 131 -

132.Python编码 20. sum += abs(item - median) 21. return sum / len(alist) 可以看到,getMedian函数对列表进行了排序,由于数据量并不大,所以这种方式是可以接受的。 如果要对代码进行优化,我们可以使用选择算法。 现在,我们已经将数据从athletesTrainingSet.txt读取出来,并保存为以下形式: 1. [('Gymnastics', [54, 66], ['Asuka Teramoto']), 2. ('Basketball', [72, 162], ['Brittainey Raven']), 3. ('Basketball', [78, 204], ['Chen Nan']), 4. ('Gymnastics', [49, 90], ['Gabby Douglas']), ...] 我们需要对向量中的数据进行标准化,变成以下结果: 1. [('Gymnastics', [-1.93277, -1.21842], ['Asuka Teramoto']), 2. ('Basketball', [1.09243, 1.63447], ['Brittainey Raven']), 3. ('Basketball', [2.10084, 2.88261], ['Chen Nan']), 4. ('Gymnastics', [-2.7731, -0.50520]), 5. ('Track', [-0.08403, -0.23774], ['Helalia Johannes']), 6. ('Track', [-0.42017, -0.02972], ['Irina Miketenko']), ...] 在init方法中,添加标准化过程: 1. # 获取向量的长度 2. self.vlen = len(self.data[0][1]) 3. # 标准化 4. for i in range(self.vlen): 5. self.normalizeColumn(i) 在for循环中逐列进行标准化,即第一次会标准化身高,第二次标准化体重。 动手实践 下载normalizeColumnTemplate.py文件,编写normalizeColumn方法。 答案 1. def normalizeColumn(self, columnNumber): 2. """标准化self.data中的第columnNumber列""" 3. # 将该列的所有值提取到一个列表中 4. col = [v[1][columnNumber] for v in self.data] 5. median = self.getMedian(col) 6. asd = self.getAbsoluteStandardDeviation(col, median) 7. #print("Median: %f ASD = %f" % (median, asd)) 本文档使用 书栈(BookStack.CN) 构建 - 132 -

133.Python编码 8. self.medianAndDeviation.append((median, asd)) 9. for v in self.data: 10. v[1][columnNumber] = (v[1][columnNumber] - median) / asd 可以看到,我将计算得到的中位数和绝对偏差保存在了medianAndDeviation变量中,因为我们会 用它来标准化需要预测的向量。 比如,我要预测Kelly Miller的运动项目,她身高5尺10寸(70英寸),重140磅,即原始向量为 [70, 140],需要先进行标准化。 我们计算得到的meanAndDeviation为: 1. [(65.5, 5.95), (107.0, 33.65)] 它表示向量中第一元素的中位数为65.5,绝对偏差为5.95;第二个元素的中位数为107.0,绝对偏 差33.65。 现在我们就利用这组数据将[70, 140]进行标准化。第一个元素的标准分数是: 第二个元素为: 以下是实现它的Python代码: 1. def normalizeVector(self, v): 2. """我们已保存了每列的中位数和绝对偏差,现用它来标准化向量v""" 3. vector = list(v) 4. for i in range(len(vector)): 5. (median, asd) = self.medianAndDeviation[i] 6. vector[i] = (vector[i] - median) / asd 7. return vector 最后,我们要编写分类函数,用来预测运动员的项目: 1. classifier.classify([70, 140]) 本文档使用 书栈(BookStack.CN) 构建 - 133 -

134.Python编码 在我们的实现中,classify函数只是nearestNeighbor的一层包装: 1. def classify(self, itemVector): 2. """预测itemVector的分类""" 3. return self.nearestNeighbor(self.normalizeVector(itemVector))[1][0] 动手实践 实现nearestNeighbor函数。 答案 1. def manhattan(self, vector1, vector2): 2. """计算曼哈顿距离""" 3. return sum(map(lambda v1, v2: abs(v1 - v2), vector1, vector2)) 4. 5. def nearestNeighbor(self, itemVector): 6. """返回itemVector的近邻""" 7. return min([(self.manhattan(itemVector, item[1]), item) 8. for item in self.data]) 好了,我们用200多行代码实现了近邻分类器! 在完整的示例代码中,我提供了一个test函数,它可以对分类器程序的准确性做一个评价。 比如用它来评价上面实现的分类器: 1. - Track Aly Raisman Gymnastics 62 115 2. + Basketball Crystal Langhorne Basketball 74 190 3. + Basketball Diana Taurasi Basketball 72 163 4. ... 5. + Track Xueqin Wang Track 64 110 6. + Track Zhu Xiaolin Track 67 123 本文档使用 书栈(BookStack.CN) 构建 - 134 -

135.Python编码 7. 8. 80.00% correct 可以看到,这个分类器的准确率是80%。它对篮球运动员的预测很准确,但在预测田径和体操运动员 时出现了4个失误。 鸢尾花数据集 我们可以用鸢尾花数据集做测试,这个数据集在数据挖掘领域是比较有名的。 它是20世纪30年代Ronald Fisher对三种鸢尾花的50个样本做的测量数据(萼片和花瓣)。 Ronald Fisher是一名伟大的科学家。他对统计学做出了革命性的改进,Richard Dawkins称他为“继达尔文后最伟大生物学 家。” 鸢尾花数据集可以在这里irisTrainingSet、irisTestSet找到,你可以测试你的算法,并问自己 本文档使用 书栈(BookStack.CN) 构建 - 135 -

136.Python编码 一些问题:标准化让结果更正确了吗?训练集中的数据量越多越好吗?用欧几里得距离来算会怎样? 记住 所有的学习过程都是在你自己的脑中进行的,你付出的努力越多,学到的也就越多。 鸢尾花数据集的格式如下,我们要预测的是Species这一列: 训练集中有120条数据,测试集中有30条,两者没有交集。 测试结果如何呢? 1. >>> test('irisTrainingSet.data', 'iristestSet.data') 2. 93.33% correct 这又一次证明我们的分类算法是简单有效的。 有趣的是,如果不对数据进行标准化,它的准确率将达到100%。这个现象我们会在后续的章节中讨 论。 本文档使用 书栈(BookStack.CN) 构建 - 136 -

137.每加仑燃油可以跑多少公里? 每加仑燃油可以跑多少公里? 每加仑燃油可以跑多少公里? 每加仑燃油可以跑多少公里? 最后,我们再来测试另一个广泛使用的数据集,卡内基梅隆大学统计的汽车燃油消耗和公里数数据。 它在1983年的美国统计联合会展中使用过,大致格式如下: 这个数据集做过一些修改。(下载:mpgTrainingSet.txt、mpgTestSet.txt) 我们要预测的是加仑燃油公里数(mpg),使用的数据包括汽缸数、排气量、马力、重量、加速度 等。 数据集中有342条记录,50条测试记录,运行结果如下: 1. >>> test('mpgTrainingSet.txt', 'mpgTestSet.txt') 2. 56.00% correct 本文档使用 书栈(BookStack.CN) 构建 - 137 -

138.每加仑燃油可以跑多少公里? 如果不进行标准化,准确率将只有32%。 我们应该如何提高预测的准确率呢?改进分类算法?增加训练集?还是增加特征的数量?我们将在下一章揭晓! 本文档使用 书栈(BookStack.CN) 构建 - 138 -

139.番外篇:关于标准化 番外篇:关于标准化 番外篇:关于标准化 番外篇:关于标准化 这一章我们讲解了标准化的重要性,即当不同特征的评分尺度不一致时,为了得到更准确的距离结 果,就需要将这些特征进行标准化,使他们在同一个尺度内波动。 虽然大多数数据挖掘工程师对标准化的理解是一致的,但也有一些人要将这种做法区分为“正规 化”和“标准化”两种。 其中,“正规化”表示将值的范围缩小到0和1之间;“标准化”则是将特征值转换为均值为0的一组数, 其中每个数表示偏离均值的程度(即标准偏差或绝对偏差)。我们使用的修正的标准分就是属于后 者。 回忆一下,我们上文中有讲解过如何将特征值缩小到0到1之间:找出最大最小值,并做如下计算: 本文档使用 书栈(BookStack.CN) 构建 - 139 -

140.番外篇:关于标准化 我们来比较一下使用不同的标准化方法得到的准确度: 看来还是使用修正的标准分结果会好些。 用不同的数据集来测试我们的算法是不是很有趣? 这些数据集是从UCI机器学习仓库中获得的。去下载一些新的数据集,调整一下格式,测试我们学过 的算法吧! 本文档使用 书栈(BookStack.CN) 构建 - 140 -

141.进一步探索分类 进一步探索分类 第五章:进一步探索分类 第五章:进一步探索分类 原文:http://guidetodatamining.com/chapter5 本章会讨论如何评价分类器的效果,方法包括十折交叉验证、留一法、以及Kappa检验等,同时还会 引入kNN算法。 内容: 效果评估算法和kNN 留一法 混淆矩阵 代码示例 Kappa指标 优化近邻算法 本文档使用 书栈(BookStack.CN) 构建 - 141 -

142.效果评估算法和kNN 效果评估算法和kNN 效果评估算法和kNN 训练集和测试集 十折交叉验证 效果评估算法和kNN 让我们回到上一章中运动项目的例子。 在那个例子中,我们编写了一个分类器程序,通过运动员的身高和体重来判断她参与的运动项目——体 操、田径、篮球等。 上图中的Marissa Coleman,身高6尺1寸,重160磅,我们的分类器可以正确的进行预测: 1. >>> cl = Classifier('athletesTrainingSet.txt') 2. >>> cl.classify([73, 160]) 本文档使用 书栈(BookStack.CN) 构建 - 142 -

143.效果评估算法和kNN 3. 'Basketball' 对于身高4尺9寸,90磅重的人: 1. >>> cl.classify([59, 90]) 2. 'Gymnastics' 当我们构建完一个分类器后,应该问以下问题: 分类器的准确度如何? 结果理想吗? 如何与其它分类器做比较? 训练集和测试集 上一章我们一共引入了三个数据集,分别是女运动员、鸢尾花、加仑公里数。 我们将这些数据集分为了两个部分,第一部分用来构造分类器,因此称为训练集;另一部分用来评估 分类器的结果,因此称为测试集。 训练集和测试集在数据挖掘中很常用。 数据挖掘工程师不会用同一个数据集去训练和测试程序。 因为如果使用训练集去测试分类器,得到的结果肯定是百分之百准确的。 换种说法,在评价一个数据挖掘算法的效果时,如果用来测试的数据集是训练集本身的一个子集,那 结果会极大程度趋向于好,所以这种做法不可取。 本文档使用 书栈(BookStack.CN) 构建 - 143 -

144.效果评估算法和kNN 将数据集拆分成一大一小两个部分的做法就产生了,前者用来训练,后者用来测试。不过,这种做法 似乎也有问题:如果分割的时候不凑巧,就会引发异常。 比如,若测试集中的篮球运动员恰巧都很矮,她们就会被归为马拉松运动员;如果又矮又轻,则会被 归为体操运动员。使用这样的测试集会造成评分结果非常低。 相反的情况也有可能出现,使评分结果趋于100%准确。无论哪种情况发生,都不是一种真实的评价。 解决方法之一是将数据集按不同的方式拆分,测试多次,取结果的平均值。比如,我们将数据集拆为 均等的两份: 我们可以先用第一部分做训练集,第二部分做测试集,然后再反过来,取两次测试的平均结果。我们 还可以将数据集分成三份,用两个部分来做训练集,一个部分来做测试集,迭代三次: 1. 使用Part 1和Part 2训练,使用Part 3测试; 2. 使用Part 1和Part 3训练,使用Part 2测试; 3. 使用Part 2和Part 3训练,使用Part 1测试; 最后取三次测试的平均结果。 在数据挖掘中,通常的做法是将数据集拆分成十份,并按上述方式进行迭代测试。因此这种方式也称 为—— 十折交叉验证 将数据集随机分割成十个等份,每次用9份数据做训练集,1份数据做测试集,如此迭代10次。 我们来看一个示例:假设我有一个分类器能判断某个人是否是篮球运动员。我的数据集包含500个运 动员和500个普通人。 第一步:将数据分成10份 本文档使用 书栈(BookStack.CN) 构建 - 144 -

145.效果评估算法和kNN 每个桶中会放50个篮球运动员,50个普通人,一共100人。 第二步:重复以下步骤10次 1. 每次迭代我们保留一个桶,比如第一次迭代保留木桶1,第二次保留木桶2。 2. 我们使用剩余的9个桶来训练分类器,比如第一次迭代使用木桶2至10来训练。 3. 我们用刚才保留的一个桶来进行测试,并记录结果,比如:35个篮球运动员分类正确,29个普通 人分类正确。 第三步:合并结果 我们可以用一张表格来展示结果: 500个篮球运动员中有372个人判断正确,500个普通人中有280个人判断正确,所以我们可以认为 1000人中有652个人判断正确,准确率就是65.2%。 通过十折交叉验证得到的评价结果肯定会比二折或者三折来得准确,毕竟我们使用了90%的数据进行 训练,而非二折验证中的50%。 好,既然十折交叉验证效果那么好,我们为何不做一个N折交叉验证?N即数据集中的数据量。 如果我们有1000个数据,我们就用999个数据来训练分类器,再用它去判定剩下的一个数据。这样得到的验证效果应该是最好的。 本文档使用 书栈(BookStack.CN) 构建 - 145 -

146.效果评估算法和kNN 本文档使用 书栈(BookStack.CN) 构建 - 146 -

147.留一法 留一法 留一法 什么是确定性? 留一法的缺点 分层问题 留一法 在数据挖掘领域,N折交叉验证又称为留一法。 上面已经提到了留一法的优点之一:我们用几乎所有的数据进行训练,然后用一个数据进行测试。 留一法的另一个优点是:确定性。 什么是确定性? 试想Lucy花了一整周的时间编写了一个分类器。周五的时候她请两位同事(Emily和Li)来对这个分 类器进行测试,并给了他们相同的数据集。 这两位同事都使用十折交叉验证,结果是: Emily:这个分类器的准确率是73.69%,很不错! Li:它的准确率只有71.27%。 本文档使用 书栈(BookStack.CN) 构建 - 147 -

148.留一法 为什么她们的结果不一样?是某个人计算发生错误了吗?其实不是。 在十折交叉验证中,我们需要将数据随机等分成十份,因此Emily和Li的分法很有可能是不一样的。 这样一来,她们的训练集和测试集也都不相同了,得到的结果自然不同。 即使是同一个人进行检验,如果两次使用了不同的分法,得到的结果也会有差异。 因此,十折交叉验证是一种不确定的验证。相反,留一法得到的结果总是相同的,这是它的一个优 点。 留一法的缺点 最大的缺点是计算时间很长。 假设我们有一个包含1000条记录的数据集,使用十折交叉验证需要运行10分钟,而使用留一法则需要 16个小时。如果我们的数据集更大,达到百万级,那检验的时间就更长了。 我两年后再给你检验结果! 留一法的另一个缺点是分层问题。 分层问题 让我们回到运动员分类的例子——判断女运动员参与的项目是篮球、体操、还是田径。 在训练分类器的时候,我们会试图让训练集包含全部三种类别。如果我们完全随机分配,训练集中有 本文档使用 书栈(BookStack.CN) 构建 - 148 -

149.留一法 可能会不包含篮球运动员,在测试的时候就会影响结果。 比如说,我们来构建一个包含100个运动员的数据集:从女子NBA网站上获取33名篮球运动员的信 息,到Wikipedia上获取33个参加过2012奥运会体操项目的运动员,以及34名田径运动员的信息。 这个数据集看起来是这样的: 现在我们来做十折交叉验证。我们按顺序将这些运动员放到10个桶中,所以前三个桶放的都是篮球运 动员,第四个桶有篮球运动员也有体操运动员,以此类推。 这样一来,没有一个桶能真正代表这个数据集的全貌。最好的方法是将不同类别的运动员按比例分发 到各个桶中,这样每个桶都会包含三分之一篮球运动员、三分之一体操运动员、以及三分之一田径运 动员。 这种做法叫做分层。而在留一法中,所有的测试集都只包含一个数据。所以说,留一法对小数据集是 合适的,但大多数情况下我们会选择十折交叉验证。 本文档使用 书栈(BookStack.CN) 构建 - 149 -

150.混淆矩阵 混淆矩阵 混淆矩阵 混淆矩阵 目前我们衡量分类器准确率的方式是使用以下公式:正确分类的记录数÷记录总数。 有时我们会需要一个更为详细的评价结果,这时就会用到一个称为混淆矩阵的可视化表格。 表格的行表示测试用例实际所属的类别,列则表示分类器的判断结果。 混淆矩阵可以帮助我们快速识别出分类器到底在哪些类别上发生了混淆,因此得名。 让我们看看运动员的示例,这个数据集中有300人,使用十折交叉验证,其混淆矩阵如下: 可以看到,100个体操运动员中有83人分类正确,17人被错误地分到了马拉松一列;92个篮球运动员 本文档使用 书栈(BookStack.CN) 构建 - 150 -

151.混淆矩阵 分类正确,8人被分到了马拉松;85个马拉松运动员分类正确,9人被分到了体操,16人被分到了篮 球。 混淆矩阵的对角线(绿色字体)表示分类正确的人数,因此求得的准确率是: 从混淆矩阵中可以看出分类器的主要问题。 在这个示例中,我们的分类器可以很好地区分体操运动员和篮球运动员,而马拉松运动员则比较容易 和其他两个类别发生混淆。 怎样,是不是觉得混淆矩阵其实并不混淆呢? 本文档使用 书栈(BookStack.CN) 构建 - 151 -

152.代码示例 代码示例 代码示例 代码示例 让我们使用加仑公里数这个数据集,格式如下: 我会通过汽车的以下属性来判断它的加仑公里数:汽缸数、排气量、马力、重量、加速度。我将392 条数据都存放在mpgData.txt文件中,并用下面这段Python代码将这些数据按层次等分成十份: 1. # -*- coding: utf-8 -*- 2. # 3. # 将数据等分成十份的示例代码 4. 5. import random 6. 7. def buckets(filename, bucketName, separator, classColumn): 8. """filename是源文件名 9. bucketName是十个目标文件的前缀名 10. separator是分隔符,如制表符、逗号等 11. classColumn是表示数据所属分类的那一列的序号""" 12. 13. # 将数据分为10份 14. numberOfBuckets = 10 15. data = {} 16. # 读取数据,并按分类放置 17. with open(filename) as f: 18. lines = f.readlines() 19. for line in lines: 20. if separator != '\t': 21. line = line.replace(separator, '\t') 22. # 获取分类 23. category = line.split()[classColumn] 24. data.setdefault(category, []) 25. data[category].append(line) 本文档使用 书栈(BookStack.CN) 构建 - 152 -

153.代码示例 26. # 初始化分桶 27. buckets = [] 28. for i in range(numberOfBuckets): 29. buckets.append([]) 30. # 将各个类别的数据均匀地放置到桶中 31. for k in data.keys(): 32. # 打乱分类顺序 33. random.shuffle(data[k]) 34. bNum = 0 35. # 分桶 36. for item in data[k]: 37. buckets[bNum].append(item) 38. bNum = (bNum + 1) % numberOfBuckets 39. 40. # 写入文件 41. for bNum in range(numberOfBuckets): 42. f = open("%s-%02i" % (bucketName, bNum + 1), 'w') 43. for item in buckets[bNum]: 44. f.write(item) 45. f.close() 46. 47. # 调用示例 48. buckets("pimaSmall.txt", 'pimaSmall',',',8) 执行这个程序后会生成10个文件:mpgData01、mpgData02等。 编程实践 你能否修改上一章的近邻算法程序,让 test 函数能够执行十折交叉验证?输出的结果应该是这样 的: 本文档使用 书栈(BookStack.CN) 构建 - 153 -

154.代码示例 解决方案 我们需要进行以下几步: 修改初始化方法,只读取九个桶中的数据作为训练集; 增加一个方法,从第十个桶中读取测试集; 执行十折交叉验证。 下面我们分步来看: 初始化方法 __init__ __init__ 方法的签名会修改成以下形式: 1. def __init__(self, bucketPrefix, testBucketNumber, dataFormat): 每个桶的文件名是mpgData-01、mpgData-02这样的形式,所以 bucketPrefix 就 是“mpgData”。 testBucketNumber 是测试集所用的桶,如果是3,则分类器会使用1、2、4-9的桶进 行训练。 dataFormat 用来指定数据集的格式,如: 1. class num num num num num comment 意味着第一列是所属分类,后五列是特征值,最后一列是备注信息。 本文档使用 书栈(BookStack.CN) 构建 - 154 -

155.代码示例 以下是初始化方法的示例代码: 1. class Classifier: 2. 3. def __init__(self, bucketPrefix, testBucketNumber, dataFormat): 4. """该分类器程序将从bucketPrefix指定的一系列文件中读取数据, 5. 并留出testBucketNumber指定的桶来做测试集,其余的做训练集。 6. dataFormat用来表示数据的格式,如: 7. "class num num num num num comment" 8. """ 9. 10. self.medianAndDeviation = [] 11. 12. # 从文件中读取文件 13. 14. self.format = dataFormat.strip().split('\t') 15. self.data = [] 16. # 用1-10来标记桶 17. for i in range(1, 11): 18. # 判断该桶是否包含在训练集中 19. if i != testBucketNumber: 20. filename = "%s-%02i" % (bucketPrefix, i) 21. f = open(filename) 22. lines = f.readlines() 23. f.close() 24. for line in lines[1:]: 25. fields = line.strip().split('\t') 26. ignore = [] 27. vector = [] 28. for i in range(len(fields)): 29. if self.format[i] == 'num': 30. vector.append(float(fields[i])) 31. elif self.format[i] == 'comment': 32. ignore.append(fields[i]) 33. elif self.format[i] == 'class': 34. classification = fields[i] 35. self.data.append((classification, vector, ignore)) 36. self.rawData = list(self.data) 37. # 获取特征向量的长度 38. self.vlen = len(self.data[0][1]) 39. # 标准化数据 40. for i in range(self.vlen): 41. self.normalizeColumn(i) testBucket方法 下面的方法会使用一个桶的数据进行测试: 本文档使用 书栈(BookStack.CN) 构建 - 155 -

156.代码示例 1. def testBucket(self, bucketPrefix, bucketNumber): 2. """读取bucketPrefix-bucketNumber所指定的文件作为测试集""" 3. 4. filename = "%s-%02i" % (bucketPrefix, bucketNumber) 5. f = open(filename) 6. lines = f.readlines() 7. totals = {} 8. f.close() 9. for line in lines: 10. data = line.strip().split('\t') 11. vector = [] 12. classInColumn = -1 13. for i in range(len(self.format)): 14. if self.format[i] == 'num': 15. vector.append(float(data[i])) 16. elif self.format[i] == 'class': 17. classInColumn = i 18. theRealClass = data[classInColumn] 19. classifiedAs = self.classify(vector) 20. totals.setdefault(theRealClass, {}) 21. totals[theRealClass].setdefault(classifiedAs, 0) 22. totals[theRealClass][classifiedAs] += 1 23. return totals 比如说bucketPrefix是mpgData,bucketNumber是3,那么程序会从mpgData-03中读取内容, 作为测试集。这个方法会返回如下形式的结果: 1. {'35': {'35': 1, '20': 1, '30': 1}, 2. '40': {'30': 1}, 3. '30': {'35': 3, '30': 1, '45': 1, '25': 1}, 4. '15': {'20': 3, '15': 4, '10': 1}, 5. '10': {'15': 1}, 6. '20': {'15': 2, '20': 4, '30': 2, '25': 1}, 7. '25': {'30': 5, '25': 3}} 这个字段的键表示真实类别。如第一行的35表示该行数据的真实类别是35加仑公里。这个键又对应一 个字典,这个字典表示的是分类器所判断的类别,如: 1. '15': {'20': 3, '15': 4, '10': 1}, 其中的3表示有3条记录真实类别是15加仑公里,但被分类到了20加仑公里;4表示分类正确的记录 数;1表示被分到10加仑公里的记录数。 执行十折交叉验证 本文档使用 书栈(BookStack.CN) 构建 - 156 -

157.代码示例 最后我们需要编写一段程序来执行十折交叉验证,也就是说要用不同的训练集和测试集来构建10个分 类器。 1. def tenfold(bucketPrefix, dataFormat): 2. results = {} 3. for i in range(1, 11): 4. c = Classifier(bucketPrefix, i, dataFormat) 5. t = c.testBucket(bucketPrefix, i) 6. for (key, value) in t.items(): 7. results.setdefault(key, {}) 8. for (ckey, cvalue) in value.items(): 9. results[key].setdefault(ckey, 0) 10. results[key][ckey] += cvalue 11. 12. # 输出结果 13. categories = list(results.keys()) 14. categories.sort() 15. print( "\n Classified as: ") 16. header = " " 17. subheader = " +" 18. for category in categories: 19. header += category + " " 20. subheader += "----+" 21. print (header) 22. print (subheader) 23. total = 0.0 24. correct = 0.0 25. for category in categories: 26. row = category + " |" 27. for c2 in categories: 28. if c2 in results[category]: 29. count = results[category][c2] 30. else: 31. count = 0 32. row += " %2i |" % count 33. total += count 34. if c2 == category: 35. correct += count 36. print(row) 37. print(subheader) 38. print("\n%5.3f percent correct" %((correct * 100) / total)) 39. print("total of %i instances" % total) 40. 41. # 调用方法 42. tenfold("mpgData/mpgData", "class num num num num num comment") 执行结果如下: 本文档使用 书栈(BookStack.CN) 构建 - 157 -

158.代码示例 1. Classified as: 2. 10 15 20 25 30 35 40 45 3. +----+----+----+----+----+----+----+----+ 4. 10 | 3 | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 5. 15 | 3 | 68 | 14 | 1 | 0 | 0 | 0 | 0 | 6. 20 | 0 | 14 | 66 | 9 | 5 | 1 | 1 | 0 | 7. 25 | 0 | 1 | 14 | 35 | 21 | 6 | 1 | 1 | 8. 30 | 0 | 1 | 3 | 17 | 21 | 14 | 5 | 2 | 9. 35 | 0 | 0 | 2 | 8 | 9 | 14 | 4 | 1 | 10. 40 | 0 | 0 | 1 | 0 | 5 | 5 | 0 | 0 | 11. 45 | 0 | 0 | 0 | 2 | 1 | 1 | 0 | 2 | 12. +----+----+----+----+----+----+----+----+ 13. 14. 53.316 percent correct 15. total of 392 instances 可以在这里下载代码和数据集。 本文档使用 书栈(BookStack.CN) 构建 - 158 -

159.Kappa指标 Kappa指标 Kappa指标 Kappa指标 本章的开头我们对分类器的效果提了几个问题,并在此之后使用十折交叉验证和混淆矩阵来对分类器 进行评估。 上一节中我们对加仑公里数分类器的评价结果是53.316%的正确率,那这个结果是好是坏呢? 我们就需要使用一个新的指标:Kappa指标。 Kappa指标可以用来评价分类器的效果比随机分类要好多少。 我们仍用运动员的例子来说明,以下是它的混淆矩阵: 本文档使用 书栈(BookStack.CN) 构建 - 159 -

160.Kappa指标 我增加了“合计”一列,因此在计算正确率时,我们只需将对角线相加(35 + 88 + 28 = 151)除 以合计(200)就可以了,结果是0.755。 现在,我们建造另一个混淆矩阵,用来表示随机分类的结果。 首先,我们将上表中的数据抹去一部分,只留下合计: 从最后一行可以看到,我们之前构造的分类器将50%的运动员分类到篮球运动员中(200中的100 人),20%分到了体操,剩余30%分到了马拉松。即: 体操 20% 篮球 50% 田径 30% 我们会用这个百分比来构造随机分类器的混淆矩阵。 比如,真实的体操运动员一共有60人,随机分类器会将其中的20%(12人)分类为体操,50%(30 人)分类为篮球,30%(18人)分类为马拉松,填入表格: 继续用这种方法填充空白。 100个真实的篮球运动员,20%(20人)分到体操,50%(50人)分到篮球,30%(30人)分到马拉 松。 本文档使用 书栈(BookStack.CN) 构建 - 160 -

161.Kappa指标 从而得到随机分类器的准确率是: Kappa指标可以用来衡量我们之前构造的分类器和随机分类器的差异,公式为: P(c)表示分类器的准确率,P(r)表示随机分类器的准确率。将之前的结果代入公式: 0.61要如何解释呢?可以参考下列经验结果: 来源:Landis, JR, Koch, GG. 1977 分类效果评估 生物测量学 动手实践 假设我们开发了一个效果不太好的分类器,用来判断600名大学生所读专业,使用的数据是他们对10 部电影的评价。 本文档使用 书栈(BookStack.CN) 构建 - 161 -

162.Kappa指标 这些大学生的专业类别有计算机科学(cs)、教育学(ed)、英语(eng)、心理学(psych)。 以下是该分类器的混淆矩阵,尝试计算出它的Kappa指标并予以解释。 准确率 = 0.697 解答 首先,计算列合计和百分比: 然后根据百分比来填充随机分类器的混淆矩阵: 本文档使用 书栈(BookStack.CN) 构建 - 162 -

163.Kappa指标 准确率 = (8 + 24 + 51 + 92) / 600 = (175 / 600) = 0.292 最后,计算Kappa指标: 这说明分类器的效果还是要好过预期的。 本文档使用 书栈(BookStack.CN) 构建 - 163 -

164.优化近邻算法 优化近邻算法 优化近邻算法 kNN算法 优化近邻算法 有一种分类器叫“机械记忆分类器(Rote Classifer)”,它会将数据集完整地保存下来,并用来判 断某条记录是否存在于数据集中。 所以,如果我们只对数据集中的数据进行分类,准确率将是100%。而在现实应用中,这种分类器并不 可用,因为我们需要判定某条新的记录属于哪个分类。 你可以认为我们上一章中构建的分类器是机械记忆分类器的一种扩展,只是我们不要求新的记录完全 对应到数据集中的某一条记录,只要距离最近就可以了。 PangNing Tan等人在其机器学习的教科书中写过这样一段话:如果一只动物走起来像鸭子、叫起来 像鸭子、而且看起来也像鸭子,那它很有可能就是一只鸭子。 近邻算法的问题之一是异常数据。还是拿运动员分类举例,不过只看体操和马拉松。 假设有一个比较矮也比较轻的人,她是马拉松运动员,这样就会形成以下这张图,横轴是体重,纵轴 是身高,其中m表示马拉松,g表示体操: 本文档使用 书栈(BookStack.CN) 构建 - 164 -

165.优化近邻算法 可以看到这名特别的马拉松运动员处于体操运动员的群组中。 假设我们要对一名新的运动员进行分类,用图中的x表示,可以看到离她最近的运动员是那名特别的马 拉松运动员,这样一来这名新的运动员就会被归到马拉松,但实际上她更有可能是一名体操运动员。 kNN算法 优化方法之一是考察这条新记录周围距离最近的k条记录,而不是只看一条,因此这种方法称为k近邻 算法(kNN)。 每个近邻都有投票权,程序会将新记录判定为得票数最多的分类。比如说,我们使用三个近邻(k = 3),其中两条记录属于体操,一条记录属于马拉松,那我们会判定x为体操。 本文档使用 书栈(BookStack.CN) 构建 - 165 -

166.优化近邻算法 因此,我们在判定一条记录的具体分类时可以用这种投票的方法。如果两个分类的票数相等,就随机 选取一个。 但对于需要预测具体数值的情形,比如一个人对Funky Meters乐队的评分,我们可以计算k个近邻 的距离加权平均值。 举个例子,我们需要预测Ben对Funky Meters的喜好程度,他的三个近邻分别是Sally、Tara、和 Jade。 下表是这三个人离Ben的距离,以及他们对Funky Meters的评分: 可以看到,Sally离Ben最近,她给Funky Meters的评分是4。 在计算平均值的时候,我希望距离越近的用户影响越大,因此可以对距离取倒数,从而得到下表: 本文档使用 书栈(BookStack.CN) 构建 - 166 -

167.优化近邻算法 下面,我们把所有的距离倒数除以距离倒数的和(0.2 + 0.1 + 0.067 = 0.367),从而得到评 分的权重: 我们可以注意到两件事情:权重之和是1;原始数据中,Sally的距离是Tara的二分之一,这点在权 重中体现出来了。 最后,我们求得平均值,也即预测Ben对Funky Meters的评分: 动手实践 我们想要预测Sofia对爵士钢琴手Hiromi的评分,以下是她三个近邻的距离和评分: 解答 本文档使用 书栈(BookStack.CN) 构建 - 167 -

168.优化近邻算法 第一步,计算距离的倒数: 第二步,计算权重(距离倒数之和为0.475): 第三步,预测评分: 本文档使用 书栈(BookStack.CN) 构建 - 168 -

169.新的数据集,新的挑战 新的数据集,新的挑战 新的数据集,新的挑战! 更多数据,更好的算法,还有抛锚的巴士 新的数据集,新的挑战! 是时候使用新的数据集了——比马印第安人糖尿病数据集,由美国国家糖尿病、消化和肾脏疾病研究所 提供。 令人惊讶的是,超过30%的比马人患有糖尿病,而全美的糖尿病患者比例是8.3%,中国只有4.2%。 数据集中的一条记录代表一名21岁以上的比马女性,她们分类两类:五年内查出患有糖尿病,以及没 有得病。 共选取了8个特征: 1. 怀孕次数; 2. 口服葡萄糖耐量实验两小时后的血浆葡萄糖浓度; 本文档使用 书栈(BookStack.CN) 构建 - 169 -

170.新的数据集,新的挑战 3. 舒张压(mm Hg); 4. 三头肌皮褶厚度(mm); 5. 血清胰岛素(mu U/ml); 6. 身体质量指数(BMI):体重(公斤)除以身高(米)的平方; 7. 糖尿病家谱; 8. 年龄(岁)。 以下是示例数据,最后一列的0表示没有糖尿病,1表示患有糖尿病: 比如说,第一条记录表示一名生过两次小孩的女性,她的血糖浓度是99,舒张压是52,等等。 实践[1] 本书提供了两份数据集:pimaSmall.zip和pima.zip。前者包含100条记录,后者包含393条记 录,都已经等分成了10个文件(10个桶)。我用前面实现的近邻算法计算了pimaSmall数据集,得 到的结果如下: 提示:代码中的 heapq.nsmallest(n, list) 会返回n个最小的项。heapq 是Python内置的优 本文档使用 书栈(BookStack.CN) 构建 - 170 -

171.新的数据集,新的挑战 先队列类库。 你的任务是实现kNN算法。 首先在类的init函数中添加参数k: 1. def __init__(self, bucketPrefix, testBucketNumber, dataFormat, k): knn函数的签名应该是: 1. def knn(self, itemVector): 它会使用到 self.k (记得在init函数中保存这个值),它的返回值是0或1。 此外,在进行十折交叉验证(tenFold函数)时也要传入k参数。 解答 init函数的修改很简单: 1. def __init__(self, bucketPrefix, testBucketNumber, dataFormat, k): 2. self.k = k 3. ... knn函数的实现是: 1. def knn(self, itemVector): 2. """使用kNN算法判断itemVector所属类别""" 3. # 使用heapq.nsmallest来获得k个近邻 4. neighbors = heapq.nsmallest(self.k, 5. [(self.manhattan(itemVector, item[1]), item) 6. for item in self.data]) 7. # 每个近邻都有投票权 8. results = {} 9. for neighbor in neighbors: 10. theClass = neighbor[1][0] 11. results.setdefault(theClass, 0) 12. results[theClass] += 1 13. resultList = sorted([(i[1], i[0]) for i in results.items()], reverse=True) 14. # 获取得票最高的分类 15. maxVotes = resultList[0][0] 16. possibleAnswers = [i[1] for i in resultList if i[0] == maxVotes] 17. # 若得票相等则随机选取一个 18. answer = random.choice(possibleAnswers) 19. return(answer) 本文档使用 书栈(BookStack.CN) 构建 - 171 -

172.新的数据集,新的挑战 对tenFold函数的改动如下: 1. def tenfold(bucketPrefix, dataFormat, k): 2. results = {} 3. for i in range(1, 11): 4. c = Classifier(bucketPrefix, i, dataFormat, k) 5. ... 你可以从网站上下载这些代码,不过我的代码并不一定是最优的,仅供参考。 实践[2] 在分类效果上,究竟是数据量的多少比较重要(即使用pimaSmall和pima数据集的效果),还是更 好的算法比较重要(k=1和k=3)? 解答 以下是比较结果: 看来增加数据量要比使用更好的算法带来的效果好。 实践[3] 72.519%的准确率看起来不错,但还是用Kappa指数来检验一下吧: 本文档使用 书栈(BookStack.CN) 构建 - 172 -

173.新的数据集,新的挑战 解答 计算合计和比例: 随机分类器的混淆矩阵: 随机分类器的正确率: Kappa指标: 效果一般 更多数据,更好的算法,还有抛锚的巴士 本文档使用 书栈(BookStack.CN) 构建 - 173 -

174.新的数据集,新的挑战 几年前我去参加一个墨西哥城的研讨会,比较特别的是会议的第二天是坐观光巴士旅游,观看黑脉金 斑蝶等。这辆巴士比较破旧,中途抛锚了好几次,所以我们一群有着博士学位的人就站在路边一边谈 笑,一边等着司机修理巴士。而事实证明这段经历是这次会议最大的收获。 其间,我有幸与Eric Brill做了交流,他在词性分类方面有着很高的成就,他的算法比前人要优秀 很多,从而使他成为自然语言处理界的名人。我和他谈论了分类器的效果问题,他说实验证明增加数 据所带来的效果要比改进算法来得大。 事实上,如果仍沿用老的词性分类算法,而仅仅增加训练集的数据量,效果很有可能比他现有的算法 更好。当然,他不可能通过收集更多的数据来获得一个博士学位,但如果如果你的算法能够取得哪怕 一点点改进,也足够了。 当然,这并不是说你就不需要挑选出更好的算法了,我们之前也看到了好的算法所带来的效果也是惊 人的。 但是如果你只是想解决一个问题,而非发表一篇论文,那增加数据量会更经济一些。 所以,在认同数据量多寡的重要影响后,我们仍将继续学习各种算法。 人们使用kNN算法来做以下事情: 在亚马逊上推荐商品 评估用户的信用 通过图像分析来分类路虎车型 人像识别 分析照片中人物的性别 推荐网页 推荐旅游套餐 本文档使用 书栈(BookStack.CN) 构建 - 174 -

175.新的数据集,新的挑战 本文档使用 书栈(BookStack.CN) 构建 - 175 -

176.朴素贝叶斯 朴素贝叶斯 第六章:概率和朴素贝叶斯 第六章:概率和朴素贝叶斯 原文:http://guidetodatamining.com/chapter6 我们会在这章探索朴素贝叶斯分类算法,使用概率密度函数来处理数值型数据。 内容: 朴素贝叶斯 微软购物车 贝叶斯法则 为什么我们需要贝叶斯法则? i100、i500健康手环 使用Python编写朴素贝叶斯分类器 共和党还是民主党 数值型数据 使用Python实现 本文档使用 书栈(BookStack.CN) 构建 - 176 -

177.朴素贝叶斯 朴素贝叶斯 朴素贝叶斯 概率 朴素贝叶斯 还是让我们回到运动员的例子。如果我问你Brittney Griner的运动项目是什么,她有6尺8寸高, 207磅重,你会说“篮球”;我再问你对此分类的准确度有多少信心,你会回答“非常有信心”。 我再问你Heather Zurich,6尺1寸高,重176磅,你可能就不能确定地说她是打篮球的了,至少不 会像之前判定Brittney那样肯定。因为从Heather的身高体重来看她也有可能是跑马拉松的。 最后,我再问你Yumiko Hara的运动项目,她5尺4寸高,95磅重,你也许会说她是跳体操的,但也 不太敢肯定,因为有些马拉松运动员也是类似的身高体重。 使用近邻算法时,我们很难对分类结果的置信度进行量化。但如果使用的是基于概率的分类算法——贝 叶斯算法——那就可以给出分类结果的可能性了:这名运动员有80%的几率是篮球运动员;这位病人有 40%的几率患有糖尿病;拉斯克鲁塞斯24小时内有雨的概率是10%。 近邻算法又称为被动学习算法。这种算法只是将训练集的数据保存起来,在收到测试数据时才会进行 计算。如果我们有10万首音乐,那每进行一次分类,都需要遍历这10万条记录才行。 本文档使用 书栈(BookStack.CN) 构建 - 177 -

178.朴素贝叶斯 贝叶斯算法则是一种主动学习算法。它会根据训练集构建起一个模型,并用这个模型来对新的记录进 行分类,因此速度会快很多。 所以说,贝叶斯算法的两个优点即:能够给出分类结果的置信度;以及它是一种主动学习算法。 概率 相信大多数人对概率并不陌生。比如投掷一个硬币,正面出现的概率是50%;掷骰子,出现1点的概率 是16.7%;从一群19岁的青少年中随机挑出一个,让你说出她是女生的可能性,你会回答50%。 以上这些我们用符号P(h)来表示,即事件h发生的概率: 投掷硬币:P(正面) = 0.5 掷骰子:P(1) = 1/6 青少年:P(女生) = 0.5 如果我再告诉你一些额外的信息,比如这群19岁的青少年都是弗兰科学院建筑专业的学生,于是你到 Google上搜索后发现这所大学的女生占86%,这时你就会改变你的答案——女生的可能性是86%。 这一情形我们用P(h|D)来表示,即D条件下事件h发生的概率。比如: 本文档使用 书栈(BookStack.CN) 构建 - 178 -

179.朴素贝叶斯 P(女生|弗兰克学院的学生) = 0.86 计算的公式是: 再举一个例子,下表是一些人使用笔记本电脑和手机的品牌: 使用iPhone的概率是多少? 如果已知这个人使用的是Mac笔记本,那他使用iPhone的概率是? 本文档使用 书栈(BookStack.CN) 构建 - 179 -

180.朴素贝叶斯 首先计算出同时使用Mac和iPhone的概率: 使用Mac的概率则是: 从而计算得到Mac用户中使用iPhone的概率: 以上是正规的解法,不过为了简单起见,我们可以直接通过计数得到: 练习 iPhone用户中使用Mac的概率是? 术语 本文档使用 书栈(BookStack.CN) 构建 - 180 -

181.朴素贝叶斯 P(h)表示事件h发生的概率,称为h的先验概率。在我们进行任何计算之前就已经得知人们使用Mac的 概率是0.6。计算之后我们可能会得知使用Mac的人同时会使用iPhone。 P(h|d)称为后验概率,表示在观察了数据集d之后,h事件发生的概率是多少。比如说,我们在观察 了使用iPhone的用户后可以得出他们使用Mac的概率是多少。后验概率又称为条件概率。 在构建一个贝叶斯分类器前,我们还需要两个概率:P(D)和P(D|h),请看下面的示例。 本文档使用 书栈(BookStack.CN) 构建 - 181 -

182.微软购物车 微软购物车 微软购物车 微软购物车 你听说过微软的智能购物车吗?没错,他们真有这样的产品。这个产品是微软和一个名为Chaotic Moon的公司合作开发的。 这家公司的标语是“我们比你聪明,我们比你有创造力。”你可以会觉得这样的标语有些狂妄自大,这 里暂且不谈。 这种购物车由以下几个部分组成:Windows 8平板电脑、Kinect体感设备、蓝牙耳机(购物车可以 和你说话)、以及电动装置(购物车可以跟着你走)。 你走进一家超市,持有一张会员卡,智能购物车会识别出你,它会记录你的购物记录(当然也包括其 他人的)。 智能购物车也会显示广告(比如日本的Sensha绿茶),不过它只会向那些有可能购买此物品的用户进 行展示。 以下是一些数据示例: 本文档使用 书栈(BookStack.CN) 构建 - 182 -

183.微软购物车 P(D)表示从训练集数据中计算得到的概率,比如上表中邮编为88005的概率是: P(88005) = 0.5 P(D|h)表示在一定条件下的观察结果。比如说购买过Sencha绿茶的人中邮编为88005的概率为: 练习 没有买Sencha的人中邮编为88005的概率是? 上式中的“┐”表示取反 邮编为88001的概率是? P(88001) = 0.3 购买了Sencha的人中邮编为88001的概率? 没有购买Sencha的人中邮编为88001的概率? 本文档使用 书栈(BookStack.CN) 构建 - 183 -

184.微软购物车 本文档使用 书栈(BookStack.CN) 构建 - 184 -

185.贝叶斯法则 贝叶斯法则 贝叶斯法则 贝叶斯法则 贝叶斯法则描述了P(h)、P(h|D)、P(D)、以及P(D|h)这四个概率之间的关系: 这个公式是贝叶斯方法论的基石。在数据挖掘中,我们通常会使用这个公式去判别不同事件之间的关 系。 我们可以计算得到在某些条件下这位运动员是从事体操、马拉松、还是篮球项目的;也可以计算得到 某些条件下这位客户是否会购买Sencha绿茶等。我们会通过计算不同事件的概率来得出结论。 比如说我们要决定是否给一位客户展示Sencha绿茶的广告,已知他所在的地区邮编是88005。我们有 两个相反的假设: 这位用户会购买Sencha绿茶的概率,即:P(购买|88005); 不会购买的概率:P(┐购买|88005)。 假设我们计算得到P(购买|88005) = 0.6,而P(┐购买|88005) = 0.4,则可以认为用户会购买, 从而显示相应的广告。 再比如我们要为一家销售电子产品的公司发送宣传邮件,共有笔记本、台式机、平板电脑三种产品。 我们需要根据目标用户的类型来分别派送这三种宣传邮件。 比如我们有一位居住在88005地区的女士,她的女儿在读大学,并居住在家中,而且她还会参加瑜伽 课程。那我应该派发哪种邮件呢? 让我们用D来表示这位客户的特征: 居住在88005地区 有一个正在读大学的女儿 练习瑜伽 因此我们需要计算以下三个概率: 本文档使用 书栈(BookStack.CN) 构建 - 185 -

186.贝叶斯法则 并选择概率最大的结果。 再抽象一点,如果我们有h1, h2, …hn等事件,它们就相当于不同的类别(篮球、体操、或是有没有 患糖尿病等)。 在计算出以上这些概率后,选取最大的结果,就能用作分类了。这种方法叫最大后验估计,记为 hMAP。 我们可以用以下公式来表示最大后验估计: 本文档使用 书栈(BookStack.CN) 构建 - 186 -

187.贝叶斯法则 H表示所有的事件,所以h∈H表示“对于集合中的每一个事件”。整个公式的含义就是:对于集合中的 每一个事件,计算出P(h|D)的值,并取最大的结果。 使用贝叶斯法则进行替换: 所以我们需要计算的就是以下这个部分: 可以发现对于所有的事件,公式中的分母都是P(D),因此即便只计算P(D|h)P(h),也可以判断出最 大的结果。那么这个公式就可以简化为: 作为演示,我们选取Tom M. Mitchell《机器学习》中的例子。Tom是卡耐基梅隆大学机器学习部 的首席,也是非常友好的一个人。 这个例子是通过一次血液检验来判断某人是否患有某种癌症。已知这种癌症在美国的感染率是0.8%。 血液检验的结果有阳性和阴性两种,且存在准确性的问题:如果这个人患有癌症,则有98%的几率测 出阳性;如果他没有癌症,会有97%的几率测出阴性。 我们来尝试将这些描述语言用公式来表示: 美国有0.8%的人患有这种癌症:P(癌症) = 0.008 99.2%的人没有患有这种癌症:P(┐癌症) = 0.992 对于患有癌症的人,他的血液检测结果返回阳性的概率是98%:P(阳性|癌症) = 0.98 对于患有癌症的人,检测结果返回阴性的概率是2%:P(阴性|癌症) = 0.02 对于没有癌症的人,返回阴性的概率是97%:P(阴性|┐癌症) = 0.97 对于没有癌症的人,返回阳性的概率是3%:P(阳性|┐癌症) = 0.03 本文档使用 书栈(BookStack.CN) 构建 - 187 -

188.贝叶斯法则 Ann到医院做了血液检测,呈阳性。初看结果并不乐观,毕竟这种血液检测的准确率高达98%。那让我 们用贝叶斯法则来计算看看: P(阳性|癌症)P(癌症) = 0.98 * 0.008 = 0.0078 P(阳性|┐癌症)P(┐癌症) = 0.03 * 0.992 = 0.0298 分类的结果是她不会患有癌症。 如果想得到确切的概率,我们可以使用标准化的方法: 可以看到,血液检测为阳性的人患有这种癌症的概率是21%。 可能你会觉得这并不合情理,毕竟血液检测的准确率有98%,而结果却说Ann很可能并没有这种癌症。 事实上,很多人都会有这样的 疑问。 我来说明一下为什么会是这样的结果。很多人只看到了血液检测的准确率是98%,但没有考虑到全美 只有0.8%的人患有这种癌症。 假设我们给一个有着一百万人口的城市做血液检测,也就是说其中有8,000人患有癌症,992,000人 没有。首先,对于那8,000个癌症病人,有7,840个人的血液检测结果会呈阳性,160人会呈阴性。 本文档使用 书栈(BookStack.CN) 构建 - 188 -

189.贝叶斯法则 对于992,000人,有962,240人会呈阴性,30,000人呈阳性。将这些数字总结到下表中: Ann的测试结果呈阳性,从上表看阳性中有30,000人其实是健康的,只有7,840人确实患有癌症,所 以我们才会认为Ann很有可能是健康的。 还是没弄明白?没关系,在接触了更多练习后就会慢慢理解了。 本文档使用 书栈(BookStack.CN) 构建 - 189 -

190.为什么我们需要贝叶斯法则? 为什么我们需要贝叶斯法则? 为什么我们需要贝叶斯法则? 朴素贝叶斯 为什么我们需要贝叶斯法则? 首先回顾一下贝叶斯公式: 再看看微软购物车的数据: 比如,我们为居住在邮编为88005地区的客户设置两个事件:买或不买Sencha绿茶,即: P(h1|D) = P(买绿茶|88005) P(h2|D) = P(┐买绿茶|88005) 你也许会问,这两个概率我们都可以直接从数据中计算得到,为什么还要计算下面这个公式呢? 那是因为在现实问题中要计算P(h|D)往往是很困难的。 以上一节中的医学示例来说,我们想要根据血液测试结果来判断该人是否患有癌症: 本文档使用 书栈(BookStack.CN) 构建 - 190 -

191.为什么我们需要贝叶斯法则? P(癌症|阳性) ≈ P(阳性|癌症)P(癌症) P(┐癌症|阳性) ≈ P(阳性|┐癌症)P(┐癌症) 上面两个式子中,我们更容易计算约等号右边的结果。 比如,要计算P(阳性|癌症),我们可以对一部分已经确诊有癌症的人做血液测试;计算P(阳性|┐癌 症)时则可对确定健康的人做测试。 而P(癌症)则可直接从政府公布的官方数据中获得,P(┐癌症)更是简单的1 - P(癌症)。 但若要计算P(癌症|阳性)的话就非常有挑战性了,也就是计算出整个人群中血液测试结果呈阳性且 确诊为癌症患者的概率。 如果采用抽样法,对1000个人进行抽样测试,只有8个人患有这种癌症,这样得出的结果显然不具有 代表性,除非进一步加大抽样数量。而贝叶斯法则提供了更为简便的方法。 朴素贝叶斯 很多时候我们会用到不止一个前提条件,比如判断一个人是否会购买Sencha绿茶时可以用到顾客所在 地以及是否买过有机食品这两个条件。计算这样的概率时只需将各个条件概率相乘即可: P(买绿茶|88005 & 买有机食品) = P(88005|买绿茶)P(买有机食品|买绿茶)P(买绿茶) = 0.6 0.8 0.5 = 0.24 P(┐买绿茶|88005 & 买有机食品) = P(88005|┐买绿茶)P(买有机食品|┐买绿茶)P(┐买绿茶) = 0.4 0.25 0.5 = 0.05 所以得到的结论是居住在88005地区且买过有机食品的客户更有可能购买Sencha绿茶。这样就让我们 在智能购物车上显示广告吧! 以下是史提芬贝克对智能购物车的评价: 这种购物车的使用体验是:你取走一辆购物车,刷了会员卡,屏幕上会显示一份购物列表,里面的内容都是基于你平时的购物习惯进 行推荐的,牛奶、鸡蛋、西葫芦等等。 智能系统会提示出购买这些物品的最佳路径。另外,它还允许你修改列表中的商品,比如你可以让它不要再提示菜花和盐焗花生。 Accenture的研究表明,人们在购物时会忘记11%的原本打算购买的商品,如果有了这样的智能购物车,就可以省去客户的来回路 程,也能为超市增加销量。 本文档使用 书栈(BookStack.CN) 构建 - 191 -

192.i100、i500健康手环 i100、i500健康手环 i100、i500健康手环 i100、i500健康手环 现在我们要为iHealth公司销售健康手环产品,从而和Nike Fuel、Fitbit Flex竞争。iHealth 新出产了两件商品:i100和i500: iHealth 100 能够监测心率,使用GPS导航(从而计算每小时运动公里数等),带WiFi无线,可随时上传数据到 iHealth网站上。 iHealth 500 除了提供i100的功能外,还能监测血液含氧量等指标,且提供免费的3G网络连接到iHealth网站。 这些产品通过网络平台销售,所以iHealth雇佣我们开发一套推荐系统。我通过让购买的用户填写调 查问卷来收集数据,每个问题都对应一个特征。 比如,我们会问客户为什么要开始运动,有三个选项:健康(health)、外表(appearance)、两 者皆是(both);我们会问他目前的运动水平:很少运动(sedentary)、一般(moderate)、经 本文档使用 书栈(BookStack.CN) 构建 - 192 -

193.i100、i500健康手环 常运动(active);我们会问他对健身的热情是高(aggressive)还是一般(moderate);最 后,我们会问他是否适应使用高科技产品。 整理后的数据如下: 实践 已知一位客户的运动目的是健康、当前水平是中等、热情一般、能适应高科技产品,请用朴素贝叶斯 来推荐手环型号。 我们需要计算以下两个概率,并选取较大的结果: 1. P(i100|健康,中等水平、热情一般,适应) 2. P(i500|健康,中等水平,热情一般,适应) 我们先来看第一个概率: 1. P(i100|健康,中等水平、热情一般,适应) = P(健康|i100)P(中等水平|i100)P(热情一般|i100)P(适应|i100) 本文档使用 书栈(BookStack.CN) 构建 - 193 -

194.i100、i500健康手环 其中: 1. P(健康|i100) = 1/6 2. P(中等水平|i100) = 1/6 3. P(热情一般|i100) = 5/6 4. P(适应|i100) = 2/6 5. P(i100) = 6/15 因此: 1. P(i100|满足条件) = 0.167 * 0.167 * 0.833 * 0.333 * 0.4 = 0.00309 再计算另一个模型的概率: 1. P(i500|满足条件) = P(健康|i500)P(中等水平|i500)P(热情一般|i500)P(适应|i500) 2. = 4/9 * 3/9 * 3/9 * 6/9 * 9/15 3. = 0.444 * 0.333 * 0.333 * 0.667 * 0.6 4. = 0.01975 本文档使用 书栈(BookStack.CN) 构建 - 194 -

195.使用Python编写朴素贝叶斯分类器 使用Python编写朴素贝叶斯分类器 使用Python编写朴素贝叶斯分类器 训练 分类 使用Python编写朴素贝叶斯分类器 上例的数据格式如下: 1. both sedentary moderate yes i100 2. both sedentary moderate no i100 3. health sedentary moderate yes i500 4. appearance active moderate yes i500 5. appearance moderate aggressive yes i500 6. appearance moderate aggressive no i100 7. health moderate aggressive no i500 8. both active moderate yes i100 9. both moderate aggressive yes i500 10. appearance active aggressive yes i500 11. both active aggressive no i500 12. health active moderate no i500 13. health sedentary aggressive yes i500 14. appearance active moderate no i100 15. health sedentary moderate no i100 虽然这个这个例子中只有15条数据,但是我们还是保留十折交叉验证的过程,以便用于更大的数据 集。十折交叉验证要求数据集等分成10份,这个例子中我们简单地将15条数据全部放到一个桶里,其 它桶留空。 朴素贝叶斯分类器包含两个部分:训练和分类。 训练 训练的输出结果应该是: 先验概率,如P(i100) = 0.4; 条件概率,如P(健康|i100) = 0.167 我们使用如下代码表示先验概率: 1. self.prior = {'i500': 0.6, 'i100': 0.4} 本文档使用 书栈(BookStack.CN) 构建 - 195 -

196.使用Python编写朴素贝叶斯分类器 条件概率的表示有些复杂,用嵌套的字典来实现: 1. {'i500': {1: {'appearance': 0.3333333333333333, 'health': 0.4444444444444444, 2. 'both': 0.2222222222222222}, 3. 2: {'active': 0.4444444444444444, 'sedentary': 0.2222222222222222, 4. 'moderate': 0.3333333333333333}, 5. 3: {'aggressive': 0.6666666666666666, 'moderate': 0.3333333333333333}, 6. 4: {'yes': 0.6666666666666666, 'no': 0.3333333333333333}}, 7. 'i100': {1: {'both': 0.5, 'health': 0.16666666666666666, 8. 'appearance': 0.3333333333333333}, 9. 2: {'active': 0.3333333333333333, 'sedentary': 0.5, 10. 'moderate': 0.16666666666666666}, 11. 3: {'aggressive': 0.16666666666666666, 'moderate': 0.8333333333333334}, 12. 4: {'yes': 0.3333333333333333, 'no': 0.6666666666666666}}} 1、2、3、4表示第几列,所以第一行可以解释为购买i500的顾客中运动目的是外表的概率是 0.333。 首先我们要来进行计数,比如以下几行数据: 1. both sedentary moderate yes i100 2. both sedentary moderate no i100 3. health sedentary moderate yes i500 4. appearance active moderate yes i500 我们用字典来统计每个模型的次数,变量名为classes,逐行扫描后的结果是: 1. # 第一行 2. {'i100': 1} 3. 4. # 第二行 5. {'i100': 2} 6. 7. # 第三行 8. {'i500': 1, 'i100': 2} 9. 10. # 全部 11. {'i500': 9, 'i100': 6} 要获取模型的先验概率,只要将计数结果除以总数就可以了。 计算后验概率也需要计数,变量名为counts。这个字典较为复杂,如扫完第一行第一列的结果是: 1. {'i100': {1: {'both': 1}}} 本文档使用 书栈(BookStack.CN) 构建 - 196 -

197.使用Python编写朴素贝叶斯分类器 处理完所有数据后的计数结果是: 1. {'i500': {1: {'appearance': 3, 'health': 4, 'both': 2}, 2. 2: {'active': 4, 'sedentary': 2, 'moderate': 3}, 3. 3: {'aggressive': 6, 'moderate': 3}, 4. 4: {'yes': 6, 'no': 3}}, 5. 'i100': {1: {'both': 3, 'health': 1, 'appearance': 2}, 6. 2: {'active': 2, 'sedentary': 3, 'moderate': 1}, 7. 3: {'aggressive': 1, 'moderate': 5}, 8. 4: {'yes': 2, 'no': 4}}} 计算概率时,只需将计数除以该模型的总数就可以了: P(外表|i100) = 2 / 6 = 0.333 以下是训练用的Python代码: 1. class Classifier: 2. 3. def __init__(self, bucketPrefix, testBucketNumber, dataFormat): 4. """bucketPrefix 分桶数据集文件前缀 5. testBucketNumber 测试桶编号 6. dataFormat 数据格式,形如:attr attr attr attr class 7. """ 8. 9. total = 0 10. classes = {} 11. counts = {} 12. 13. # 从文件中读取数据 14. self.format = dataFormat.strip().split('\t') 15. self.prior = {} 16. self.conditional = {} 17. # 遍历十个桶 18. for i in range(1, 11): 19. # 跳过测试桶 20. if i != testBucketNumber: 21. filename = "%s-%02i" % (bucketPrefix, i) 22. f = open(filename) 23. lines = f.readlines() 24. f.close() 25. for line in lines: 26. fields = line.strip().split('\t') 27. ignore = [] 28. vector = [] 29. for i in range(len(fields)): 30. if self.format[i] == 'num': 本文档使用 书栈(BookStack.CN) 构建 - 197 -

198.使用Python编写朴素贝叶斯分类器 31. vector.append(float(fields[i])) 32. elif self.format[i] == 'attr': 33. vector.append(fields[i]) 34. elif self.format[i] == 'comment': 35. ignore.append(fields[i]) 36. elif self.format[i] == 'class': 37. category = fields[i] 38. # 处理该条记录 39. total += 1 40. classes.setdefault(category, 0) 41. counts.setdefault(category, {}) 42. classes[category] += 1 43. # 处理各个属性 44. col = 0 45. for columnValue in vector: 46. col += 1 47. counts[category].setdefault(col, {}) 48. counts[category][col].setdefault(columnValue, 0) 49. counts[category][col][columnValue] += 1 50. 51. # 计数结束,开始计算概率 52. 53. # 计算先验概率P(h) 54. for (category, count) in classes.items(): 55. self.prior[category] = count / total 56. 57. # 计算条件概率P(h|D) 58. for (category, columns) in counts.items(): 59. self.conditional.setdefault(category, {}) 60. for (col, valueCounts) in columns.items(): 61. self.conditional[category].setdefault(col, {}) 62. for (attrValue, count) in valueCounts.items(): 63. self.conditional[category][col][attrValue] = ( 64. count / classes[category]) 65. self.tmp = counts 分类 分类函数会这样使用: 1. c.classify(['health', 'moderate', 'moderate', 'yes']) 我们需要计算: 本文档使用 书栈(BookStack.CN) 构建 - 198 -

199.使用Python编写朴素贝叶斯分类器 1. def classify(self, itemVector): 2. """返回itemVector所属类别""" 3. results = [] 4. for (category, prior) in self.prior.items(): 5. prob = prior 6. col = 1 7. for attrValue in itemVector: 8. if not attrValue in self.conditional[category][col]: 9. # 属性不存在,返回0概率 10. prob = 0 11. else: 12. prob = prob * self.conditional[category][col][attrValue] 13. col += 1 14. results.append((prob, category)) 15. # 返回概率最高的结果 16. return(max(results)[1]) 让我们测试一下: 1. >>> c = Classifier('iHealth/i', 10, 'attr\tattr\tattr\tattr\tclass') 2. >>> c.classify(['health' 'moderate', 'moderate', 'yes']) 3. i500 本文档使用 书栈(BookStack.CN) 构建 - 199 -

200. 共和党还是民主党 共和党还是民主党 共和党还是民主党 概率估计 如何解决 一点说明 共和党还是民主党 我们来看另一个数据集——美国国会投票数据,可以从 机器学习仓库 获得。 每条记录代表一个选民,第一列是分类名称(democrat, republican),之后是16条法案,用y和 n表示该人是否支持。 1. 残疾婴儿法案 2. 用水成本分摊 3. 预算改革 4. 医疗费用 5. 萨瓦尔多援助 6. 校园宗教组织 7. 反卫星武器 8. 尼加拉瓜援助 9. MX导弹 10. 移民法案 11. 合成燃料缩减 12. 教育支出法案 13. 有毒废物基金 14. 犯罪 15. 出口免税 16. 南非出口管控 文件格式如下: 1. democrat y n y n n y y y y y n n y n n y 2. democrat y y y n n y y y y n n n n n y y 3. republican y y n y y y n n n y n y y y n n 在调用上一节编写的朴素贝叶斯分类器时使用以下dataFormat参数就可以了: 1. "class\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr\tattr" 本文档使用 书栈(BookStack.CN) 构建 - 200 -

201.共和党还是民主党 十折交叉验证的结果是: 看起来不错。 但是,这个方法有一些问题。 首先我们来模拟一个数据集,其中包含100个民主党派和100个共和党派。下表是他们对四个法案的投 票情况: 从表中可以看到,共和党人中,99%赞成网络安全法案,只有1%的人赞成读者隐私。下面我们再选取 一位议会代表——X先生,对他进行分类: 本文档使用 书栈(BookStack.CN) 构建 - 201 -

202.共和党还是民主党 你觉得X先生是民主党还是共和党呢? 我会猜测民主党。我们用朴素贝叶斯来进行分类。首先,先验概率P(民主党)和P(共和党)都是0.5, 因为样本中两党分别有100人。X先生对网络安全法案投了否决票,并且: 1. P(共和党|C=no) = 0.01 2. P(民主党|C=no) = 0.99 为了表示方便,我们用字母来表示这四个法案: 1. 网络安全:C 2. 读者隐私:R 3. 网销税负:T 4. 非法传播:S 记到表格里就是: 我们将X先生对读者隐私和网络营销税负法案的投票记到表格中: 本文档使用 书栈(BookStack.CN) 构建 - 202 -

203.共和党还是民主党 对这些概率进行标准化后可以得到: 也就是说到目前为止我们有99.99%的信心认为X先生是民主党的。 最后,我们将X先生对非法传播法案的投票记入表格: 你会惊讶地发现,X先生是民主党的可能性从99%降至0了!这是因为我们的数据集中没有一个民主党 人对网络非法传播法案投了否决票。 概率估计 使用朴素贝叶斯计算得到的概率其实是真实概率的一种估计,而真实概率是对全量数据做统计得到 的。 比如说,我们需要对所有人都做血液测试,才能得到健康人返回阴性结果的真实概率。显然,对全量 数据做统计是不现实的,所以我们会选取一个样本,如1000人,对他们进行测试并计算概率。 大部分情况下,这种估计都是接近于真实概率的。但当真实概率非常小时,这种抽样统计的做法就会 有问题了。 比如说,民主党对网络非法传播法案的否决率是0.03,即P(S=no|民主党) = 0.03。如果我们分别 选取十个民主党和共和党人,看他们对该法案的投票情况,你觉得得到的概率会是什么?答案很可能 是0。 从上一节的例子中也看到了,在朴素贝叶斯中,概率为0的影响是很大的,甚至会不顾其他概率的大 小。此外,抽样统计的另一个问题是会低估真实概率。 如何解决 我们计算P(S=no|民主党)的公式是这样的: 本文档使用 书栈(BookStack.CN) 构建 - 203 -

204.共和党还是民主党 为了表示方便,我们采用以下公式: 其中,n表示训练集中y类别的记录数;nc表示y类别中值为x的记录数。 我们的问题是上式中的nc可能为0,解决方法是将公式变为以下形式: 这个公式摘自Tom Mitchell《机器学习》的第179页。 m是一个常数,表示等效样本大小。决定常数m的方法有很多,我们这里使用值的类别来作为m,比如 投票有赞成和否决两种类别,所以m就为2。p则是相应的先验概率,比如说赞成和否决的概率分别是 0.5,那p就是0.5。 我们回到上面的例子,看看要如何应用这个方法。下表是投票的情况: X先生对网络安全法案投了否决票,我们先来计算共和党对安全法案投否决票的概率。新的公式是: 其中n是共和党的人数100,nc是共和党对安全法案投否决票的人数1,m是2,p是0.5,因此: 本文档使用 书栈(BookStack.CN) 构建 - 204 -

205.共和党还是民主党 再计算民主党: 代入之前用到的表格中: 对剩余三条法案进行计算,得到的结果是: 因此,X先生的党派是民主党,这和我们的直觉一致。 一点说明 在这个例子中,所有公式里的m都是2,但这并不表示其他数据集也是这样。比如我们之前做的健康手 环问卷调查,运动目的有三个选项,是否适应高科技则有两个选项,所以在计算运动目的概率时 m=3、p=1/3,代入公式即: 本文档使用 书栈(BookStack.CN) 构建 - 205 -

206.数值型数据 数值型数据 数值型数据 方法一:区分类别 方法二:高斯分布 总体标准差和样本标准差 代码实现提示 数值型数据 你可能已经注意到,在讨论近邻算法时,我们使用的都是数值型的数据,而在学习朴素贝叶斯算法 时,用的是分类型的数据。 比如,人们对法案的投票有赞成和否决两类;音乐家可以用他们演奏的乐器来分类等等。这些分类之 间是没有距离的,萨克斯手和钢琴家的距离并不会比鼓手近。而数值型数据则有这种远近之分。 在贝叶斯方法中,我们会对事物进行计数,这种计数则是可以度量的。对于数值型的数据要如何计数 呢?通常有两种做法: 方法一:区分类别 我们可以划定几个范围作为分类,如: 年龄 < 18 18 - 22 23 - 30 31 - 40 40 年薪 $200,000 150,000 - 200,000 100,000 - 150,000 60,000 - 100,000 40,000 - 60,000 划分类别后,进行可以应用朴素贝叶斯方法了。 方法二:高斯分布 本文档使用 书栈(BookStack.CN) 构建 - 206 -

207.数值型数据 我想将收入数据进行分类,然后应用朴素贝叶斯算法。 你的做法已经过时了,我会使用高斯分布和概率密度函数来做。 高斯分布和概率密度函数这两个词听起来很酷,他们的作用也非常大。下面我们将学习如何在朴素贝 叶斯算法中使用高斯分布。 首先,我们为健康手环的例子增加一列收入属性: 本文档使用 书栈(BookStack.CN) 构建 - 207 -

208.数值型数据 我们来看购买i500的用户的收入情况,比如取平均值: 还可以求出标准差: 标准差是用来衡量数据的离散程度的,如果所有数据都接近于平均值,那标准差也会比较小。通过公 式我们可以计算得到i500用户的收入标准差是20.108。 总体标准差和样本标准差 上面的标准差公式是总体标准差,我们需要对所有的数据进行统计才能得出,比如统计500名学生的 成绩,就能计算出总体标准差。 但通常我们无法获取总体的数据,比如要统计新墨西哥北部鹿鼠的重量,我们不可能对所有的鹿鼠进 本文档使用 书栈(BookStack.CN) 构建 - 208 -

209.数值型数据 行称重,只能选取一部分样本,这时计算得到的就是样本标准差。 样本标准差的公式是: 所以计算得到i500用户收入的样本标准差是21.327。下面的内容都会使用样本标准差。 你可能听说过正态分布、钟型曲线、高斯分布等术语,他们指的是同一件事:68%的数据会落在标准 差为1的范围内,95%的数据会落在标准差为2的范围内: 在我们的示例中,平均值是106.111,样本标准差是21.327,因此购买i500的用户中有95%的人收 入在42,660美元至149,770美元之间。如果我问你P(100k|i500)的概率有多大,你可以回答非常 大;如果问你P(20k|i500)的概率有多大,你可以回答基本不可能。 本文档使用 书栈(BookStack.CN) 构建 - 209 -

210.数值型数据 用公式来表示是: 每次出现这些公式时我都想提醒读者千万不要紧张,其实他们只是看起来比较复杂,只需一步一步拆解开就能理解了。数据挖掘学到 后面会遇到各种复杂的公式,千万不要被他们的外表吓到。 让我们逐步拆解这个公式。假设我们要计算P(100k|i500)的概率,即购买i500的用户中收入是 100,000美元的概率。之前我们计算过购买i500的用户平均收入以及样本标准差,我们用希腊字母 μ(读“谬”)来表示平均值,σ(读“西格玛”)来表示标准差。 本文档使用 书栈(BookStack.CN) 构建 - 210 -

211.数值型数据 e是自然常数,约等于2.718。 练习 下表中列出了加仑公里数为35的车型的马力(HP),现在我想知道同样是35加仑公里的Datsun 280z马力为132的概率。 结果表明这个概率非常低,而事实上这辆车就是132马力。 代码实现提示 在训练朴素贝叶斯分类器时,可以讲所有属性的平均值和样本标准差计算出来,而分类阶段使用下面 这段代码就能实现了: 本文档使用 书栈(BookStack.CN) 构建 - 211 -

212.数值型数据 1. import math 2. 3. def pdf(mean, ssd, x): 4. """概率密度函数,计算P(x|y)""" 5. ePart = math.pow(math.e, -(x - mean) ** 2 / (2 * ssd ** 2)) 6. return (1.0 / (math.sqrt(2 * math.pi) * ssd)) * ePart 测试一下: 1. >>> pdf(106.111, 21.327, 100) 2. 0.017953602706962717 3. >>> pdf(72.875, 9.804, 132) 4. 5.152283971078022e-10 休息一下吧 本文档使用 书栈(BookStack.CN) 构建 - 212 -

213.使用Python实现 使用Python实现 使用Python实现 训练阶段 朴素贝叶斯的效果要比近邻算法好吗? 使用Python实现 训练阶段 朴素贝叶斯需要用到先验概率和条件概率。让我们回顾一下民主党和共和党的例子:先验概率指的是 我们已经掌握的概率,比如美国议会中有233名共和党人,200名民主党人,那共和党人出现的概率就 是: P(共和党) = 233 / 433 = 0.54 我们用P(h)来表示先验概率。而条件概率P(h|D)则表示在已知D的情况下,事件h出现的概率。比如 说P(民主党|法案1=yes)。朴素贝叶斯公式中,我们计算的是P(D|h),如P(法案1=yes|民主党)。 在之前的Python代码中,我们用字典来表示这些概率: 1. {'democrat': {'bill 1': {'yes': 0.333, 'no': 0.667}, 2. 'bill 2': {'yes': 0.778, 'moderate': 0.222}}, 3. 'republican': {'bill 1': {'yes': 0.811, 'no': 0.189}, 4. 'bill 2': {'yes': 0.250, 'no': 0.750}}} 所以民主党中对法案1投赞成票的概率是:P(bill 1=yes|民主党) = 0.667。 对于分类型的数据,我们用上面的方法来保存概率,而对连续性的数据,我们要使用概率密度函数, 因此需要保存平均值和样本标准差。如: 1. mean = {'democrat': {'age': 57, 'years served': 12}, 2. 'republican': {'age': 53, 'years served': 7}} 3. ssd = {'democrat': {'age': 7, 'years served': 3}, 4. 'republican': {'age': 5, 'years served': 5}} 和之前一样,数据文件中的每一行表示一条记录,不同的特征值使用制表符分隔,比如下面是比马印 第安人糖尿病的数据: 本文档使用 书栈(BookStack.CN) 构建 - 213 -

214.使用Python实现 前八列是特征,最后一列是分类(1-患病,0-健康)。 我们同样需要一个格式字符串来表示每一行记录: attr 表示这一列是分类型的特征 num 表示这一列是数值型的特征 class 表示这一列是分类 对于比马数据集,格式化字符串是: 1. "num num num num num num num num class" 我们需要一个数据结构来存储平均值和样本标准差,看下面这几行数据: 为计算每一个分类的平均值,我们需要保存合计值,可以用字典来实现: 1. totals = {'1': {1: 8, 2: 378, 3: 182, 4: 102, 5: 1141, 2. 6: 98.2, 7: 2.036, 8: 141}, 3. '0': {1: 3, 2: 323, 3: 242, 4: 96, 5: 214, 4. 6: 98.1, 7: 2.006, 8: 76}} 对于分类1,第一列的合计是8(3 + 4 + 1),第二列的合计是378。 对于分类0,第一列的合计是3(2 + 1 + 0),第二列的合计是323,以此类推。 本文档使用 书栈(BookStack.CN) 构建 - 214 -

215.使用Python实现 在计算标准差时,我们还需要保留原始的值: 1. numericValues = {'1': {1: [3, 4, 1], 2: [78, 111, 189], ...}, 2. '0': {1: [2, 1, 0], 2: [142, 81, 100], ...}} 将这些逻辑添加到分类器的 __init__ 方法中: 1. import math 2. 3. class Classifier: 4. def __init__(self, bucketPrefix, testBucketNumber, dataFormat): 5. """bucketPrefix 分桶数据集文件前缀 6. testBucketNumber 测试桶编号 7. dataFormat 数据格式,形如:attr attr attr attr class 8. """ 9. total = 0 10. classes = {} 11. # 对分类型数据进行计数 12. counts = {} 13. # 对数值型数据进行求和 14. # 我们会使用下面两个变量来计算每个分类各个特征的平均值和样本标准差 15. totals = {} 16. numericValues = {} 17. 18. # 从文件中读取数据 19. self.format = dataFormat.strip().split('\t') 20. self.prior = {} 21. self.conditional = {} 22. 23. # 遍历1-10号桶 24. for i in range(1, 11): 25. # 判断是否跳过 26. if i != testBucketNumber: 27. filename = "%s-%02i" % (bucketPrefix, i) 28. f = open(filename) 29. lines = f.readlines() 30. f.close() 31. for line in lines: 32. fields = line.strip().split('\t') 33. ignore = [] 34. vector = [] 35. nums = [] 36. for i in range(len(fields)): 37. if self.format[i] == 'num': 38. nums.append(float(fields[i])) 39. elif self.format[i] == 'attr': 40. vector.append(fields[i]) 本文档使用 书栈(BookStack.CN) 构建 - 215 -

216.使用Python实现 41. elif self.format[i] == 'comment': 42. ignore.append(fields[i]) 43. elif self.format[i] == 'class': 44. category = fields[i] 45. # 处理这条记录 46. total += 1 47. classes.setdefault(category, 0) 48. counts.setdefault(category, {}) 49. totals.setdefault(category, {}) 50. numericValues.setdefault(category, {}) 51. classes[category] += 1 52. # 处理分类型数据 53. col = 0 54. for columnValue in vector: 55. col += 1 56. counts[category].setdefault(col, {}) 57. counts[category][col].setdefault(columnValue, 0) 58. counts[category][col][columnValue] += 1 59. # 处理数值型数据 60. col = 0 61. for columnValue in nums: 62. col += 1 63. totals[category].setdefault(col, 0) 64. #totals[category][col].setdefault(columnValue, 0) 65. totals[category][col] += columnValue 66. numericValues[category].setdefault(col, []) 67. numericValues[category][col].append(columnValue) 68. 69. # 计算先验概率P(h) 70. for (category, count) in classes.items(): 71. self.prior[category] = count / total 72. 73. # 计算条件概率P(h|D) 74. for (category, columns) in counts.items(): 75. self.conditional.setdefault(category, {}) 76. for (col, valueCounts) in columns.items(): 77. self.conditional[category].setdefault(col, {}) 78. for (attrValue, count) in valueCounts.items(): 79. self.conditional[category][col][attrValue] = ( 80. count / classes[category]) 81. self.tmp = counts 82. 83. # 计算平均值和样本标准差 84. self.means = {} 85. self.ssd = {} 86. # 动手实践 动手实践[1] 本文档使用 书栈(BookStack.CN) 构建 - 216 -

217.使用Python实现 为上述代码实现计算平均值和样本标准差的逻辑,输出的结果如下: 1. >>> c = Classifier('pimaSmall/pimaSmall', 1, 'num\tnum\tnum\tnum\tnum\tnum\tnum\tnum\tclass') 2. >>> c.ssd 3. {'1': {1: 4.21137914295475, 2: 29.52281872377408, ...}, 4. '0': {1: 2.54694671925252, 2: 23.454755259159146, ...}} 5. >>> c.means 6. {'1': {1: 5.25, 2: 146.05555555555554, ...}, 7. '0': {1: 2.8867924528301887, 2: 111.90566037735849, ...}} 解答 1. # 计算平均值和样本标准差 2. self.means = {} 3. self.ssd = {} 4. 5. for (category, columns) in totals.items(): 6. self.means.setdefault(category, {}) 7. for (col, cTotal) in columns.items(): 8. self.means[category][col] = cTotal / classes[category] 9. 10. for (category, columns) in numericValues.items(): 11. self.ssd.setdefault(category, {}) 12. for (col, values) in columns.items(): 13. SumOfSquareDifferences = 0 14. theMean = self.means[category][col] 15. for value in values: 16. SumOfSquareDifferences += (value - theMean)**2 17. columns[col] = 0 18. self.ssd[category][col] = math.sqrt(SumOfSquareDifferences / (classes[category] - 1)) 动手实践[2] 修改分类函数 classify() ,使其能够使用概率密度函数进行分类。 本文档使用 书栈(BookStack.CN) 构建 - 217 -

218.使用Python实现 1. def classify(self, itemVector, numVector): 2. """返回itemVector所属分类""" 3. results = [] 4. sqrt2pi = math.sqrt(2 * math.pi) 5. for (category, prior) in self.prior.items(): 6. prob = prior 7. col = 1 8. for attrValue in itemVector: 9. if not attrValue in self.conditional[category][col]: 10. # 该特征值没有出现过,因此概率给0 11. prob = 0 12. else: 13. prob = prob * self.conditional[category][col][attrValue] 14. col += 1 15. col = 1 16. for x in numVector: 17. mean = self.means[category][col] 18. ssd = self.ssd[category][col] 19. ePart = math.pow(math.e, -(x - mean)**2/(2*ssd**2)) 20. prob = prob * ((1.0 / (sqrt2pi*ssd)) * ePart) 21. col += 1 22. results.append((prob, category)) 23. # 返回概率最高的分类 24. #print(results) 25. return(max(results)[1]) 朴素贝叶斯的效果要比近邻算法好吗? 上一章中我们对近邻算法做了统计: 以下是贝叶斯算法的结果: 本文档使用 书栈(BookStack.CN) 构建 - 218 -

219.使用Python实现 哇,看来贝叶斯的效果要比近邻算法来得好呢! kNN算法中,k=3时的Kappa指标是0.35415,效果一般。那朴素贝叶斯的Kappa指标是多少呢? 贝叶斯的Kappa指标是0.4875,符合期望。 因此在这个例子中,朴素贝叶斯的效果要比近邻算法好。 贝叶斯方法的优点: 实现简单(只需计数即可) 需要的训练集较少 运算效率高 贝叶斯方法的主要缺点是无法学习特征之间的相互影响。比如我喜欢奶酪,也喜欢米饭,但是不喜欢 本文档使用 书栈(BookStack.CN) 构建 - 219 -

220.使用Python实现 两者一起吃。 kNN算法的优点: 实现也比较简单 不需要按特定形式准备数据 需要大量内存保存训练集数据 当我们的训练集较大时,kNN算法是一个不错的选择。这个算法的用途很广,包括推荐系统、蛋白质 分析、图片分类等。 为什么要叫“朴素贝叶斯”呢? 我们之所以能将多个概率进行相乘是因为这些概率都是具有独立性的。比如说,有一个游戏是同时抛 硬币和掷骰子,骰子的点数并不依赖于硬币是正面还是反面,所以在计算联合概率时可以直接相乘。 如果我们要计算同时抛出正面(heads)以及掷出6点的概率: 再比如我们有一副扑克牌,保留所有黑色牌(26张),以及红色牌中的人头牌(6张),一共32张。 那么,选出一张人头牌(facecard)的概率就是: 选出红色牌(red)的概率是: 那么,选出一张即是红色牌又是人头牌的概率是多少呢?直觉告诉我们不能这样计算: 本文档使用 书栈(BookStack.CN) 构建 - 220 -

221.使用Python实现 因为红色牌的概率是0.1875,但这张红色牌100%是人头牌,所以红色人头牌的概率应该是0.1875。 这里不能做乘法就是因为这两个事件不是互相独立的,在选择红色牌时,人头牌的概率就变了,反之 亦然。 在现实数据挖掘场景中,这种特征变量之间不独立的情况还是很多的。 运动员例子中,身高和体重不是互相独立的,因为高的人体重也会较高。 地区邮编、收入、年龄,这些特征也不完全独立,一些地区的房屋都很昂贵,一些地区则只有房 车:加州帕罗奥图大多是20岁的年轻人,而亚利桑那州则多是退休人员。 在音乐基因工程中,很多特征也是不独立的,如果音乐中有很多变音吉他,那小提琴的概率就降 低了。 血液检验的结果中,T4和TSH这两个指标通常是呈反比的。 再从你身边找找例子,比如你的车,它的各种特征之间有相关性吗?一部电影呢?亚马逊上的购买记 录又如何? 所以,在使用贝叶斯方法时,我们需要互相独立的特征,但现实生活中很难找到这样的应用,因此我 们只能假设他们是独立的了!我们完全忽略了这个问题,因此才称为“朴素的”(天真的)贝叶斯方 法。不过事实证明朴素贝叶斯的效果还是很不错的。 动手实践 你能将朴素贝叶斯方法应用到其他数据集上吗?比如加仑公里数的例子,kNN算法的正确率是53%,尝 试用贝叶斯方法来实现吧。 1. >>> tenfold('mpgData/mpgData', 'class\tattr\tnum\tnum\tnum\tnum\tcomment') 本文档使用 书栈(BookStack.CN) 构建 - 221 -

222.朴素贝叶斯算法和非结构化文本 朴素贝叶斯算法和非结构化文本 第七章:朴素贝叶斯和文本数据 第七章:朴素贝叶斯和文本数据 原文:http://guidetodatamining.com/chapter7/ 这一章我们会尝试使用朴素贝叶斯算法来对非结构化文本进行分类。我们是否能够判断出Twitter上 的一片影评是正面评价还是负面的呢? 内容: 非结构化文本的分类算法 训练阶段 使用朴素贝叶斯进行分类 新闻组语料库 朴素贝叶斯与情感分析 本文档使用 书栈(BookStack.CN) 构建 - 222 -

223.非结构化文本的分类算法 非结构化文本的分类算法 非结构化文本的分类算法 自动判别文本中的感情色彩 非结构化文本的分类算法 在前几个章节中,我们学习了如何使用人们对物品的评价(五星、顶和踩)来进行推荐;还使用了他 们的隐式评价——买过什么,点击过什么;我们利用特征来进行分类,如身高、体重、对法案的投票 等。这些数据有一个共性——能用表格来展现: 因此这类数据我们称为“结构化数据”——数据集中的每条数据(上表中的一行)由多个特征进行描述 (上表中的列)。而非结构化的数据指的是诸如电子邮件文本、推特信息、博客、新闻等。这些数据 至少第一眼看起来是无法用一张表格来展现的。 举个例子,我们想从推特信息中获取用户对各种电影的评价: 本文档使用 书栈(BookStack.CN) 构建 - 223 -

224.非结构化文本的分类算法 可以看到,Andy Gavin喜欢看地心引力,因为他的消息中有“不寒而栗”、“演的太棒了”之类的文 本。而Debra Murphy则不太喜欢这部电影,因为她说“还是省下看这部电影的钱吧”。 如果有人说“我太想看这部电影了,都兴奋坏了!”,我们可以看出她是喜欢这部电影的,即使信息中 有“坏”这个字。 我在逛超市时看到一种叫Chobani的酸奶,名字挺有趣的,但真的好吃吗?于是我掏出iPhone,谷 歌了一把,看到一篇名为“女人不能只吃面包”的博客: 无糖酸奶品评 你喝过Chobani酸奶吗?如果没有,就赶紧拿起钥匙出门去买吧!虽然它是脱脂原味的,但喝起来和酸奶的口感很像,致使我每次喝 都有负罪感,因为这分明就是在喝全脂酸奶啊! 原味的感觉很酸很够味,你也可以尝试一下蜂蜜口味的。我承认,虽然我在减肥期间不该吃蜂蜜的,但如果我有一天心情很糟想吃甜 食,我就会在原味酸奶里舀一勺蜂蜜,太值得了! 至于那些水果味的,应该都有糖分在里面,但其实酸奶本身就已经很美味了,水果只是点缀。如果你家附近没有Chobani,也可以试 试Fage,同样好吃。 虽然需要花上一美元不到,而且还会增加20卡路里,但还是很值得的,毕竟我已经一下午没吃东西了! http://womandoesnotliveonbreadalone.blogspot.com/2009/03/sugar-free-yogurt-reviews.html 这是一篇正面评价吗?从第二句就可以看出,作者非常鼓励我去买。她还用了“够味”、“美味”等词 汇,这些都是正面的评价。所以,让我先去吃会儿…… 本文档使用 书栈(BookStack.CN) 构建 - 224 -

225.非结构化文本的分类算法 自动判别文本中的感情色彩 约翰,这条推文应该是称赞地心引力的! 假设我们要构建一个自动判别文本感情色彩的系统,它有什么作用呢?比如说有家公司是售卖健康检 测设备的,他们想要知道人们对这款产品的反响如何。他们投放了很多广告,顾客是喜欢(我好想买 一台)还是讨厌(看起来很糟糕)呢? 再比如苹果公司召开了一次新闻发布会,讨论iPhone现有的问题,结果是正面的还是负面的呢?一位 参议会议员对某个法案做了一次公开演讲,那些政治评论家的反应如何?看来这个系统还是有些作用 的。 本文档使用 书栈(BookStack.CN) 构建 - 225 -

226.非结构化文本的分类算法 那要怎样构建一套这样的系统呢? 假设我要从文本中区分顾客对某些食品的喜好,可能就会列出一些表达喜欢的词语,以及表达厌恶的 词: 表达喜欢的词:美味、好吃、不错、喜欢、可口 表达厌恶的词:糟糕、难吃、不好、讨厌、恶心 比如我们想知道某篇评论对Chobani酸奶的评价是正面的还是负面的,我们可以去统计评论中表达喜 欢和厌恶的词的数量,看哪种类型出现的频率高。 这种方法也可以应用到其他分类中,比如判断某个人是否支持堕胎,如果他的言论中经常出现“未出生 的小孩”,那他很可能是反堕胎的;如果言论中出现“胎儿”这个词比较多,那有可能是支持堕胎的。 其实,用词语出现的数量来进行分类还是很容易想到的。 我们可以使用朴素贝叶斯算法来进行分类,而不是一般的计数。先来回忆一下公式: argmax表示选取概率最大的分类;h∈H表示计算每个事件的概率;P(D|h)表示在给定h的条件下,D 本文档使用 书栈(BookStack.CN) 构建 - 226 -

227.非结构化文本的分类算法 发生的概率(如给定某类文章,这类文章中特定单词出现的概率);P(h)则指事件h发生的概率。 我们的训练集是一组文本,又称为语料库。每个文本(即每条记录)是一则140字左右的推文,并被 标记为喜欢和讨厌两类。P(h)表示的就是喜欢和讨厌出现的概率。我们的训练集中有1000条记录, 喜欢和讨厌各有500条,因此它们的概率是: 1. P(喜欢) = 0.5 2. P(讨厌) = 0.5 当我们使用已经标记好分类的数据集进行训练时,这种类型的机器学习称为“监督式学习”。文本分类就是监督式学习的一种。 如果训练集没有标好分类,那就称为“非监督式学习”,聚类就是一种非监督式学习,我们将在下一章讲解。 还有一些算法结合了监督式和非监督式,通常是在初始化阶段使用分类好的数据,之后再使用未分类的数据进行学习。 让我们回到上面的公式,首先来看P(D|h)要如何计算——在正面评价中,单词D出现的概率。比如 说“Puts the Thrill back in Trhiller”这句话,我们可以统计所有表达“喜欢”的文章中第一 个单词是“Puts”的概率,第二个单词是“the”的概率,以此类推。 接着我们再计算表达“讨厌”的文章中第一个单词是“Puts”的概率,第二个单词是“the”的概率等 等。 本文档使用 书栈(BookStack.CN) 构建 - 227 -

228.非结构化文本的分类算法 谷歌曾统计过英语中大约有一百万的词汇,如果一条推文中有14个单词,那我们就需要计算1,000,00014个概率了,显然是不现实 的。 的确,这种方法并不可行。我们可以简化一下,不考虑文本中单词的顺序,仅统计表达“喜欢”的文章 中某个单词出现的概率。以下是统计方法。 本文档使用 书栈(BookStack.CN) 构建 - 228 -

229.训练阶段 训练阶段 训练阶段 训练阶段 首先,我们统计所有文本中一共出现了多少个不同的单词,记作“|Vocabulary|”(总词汇表)。 对于每个单词wk,我们将计算P(wk|hi),每个hi(喜欢和讨厌两种)的计算步骤如下: 1. 将该分类下的所有文章合并到一起; 2. 统计每个单词出现的数量,记为n; 3. 对于总词汇表中的单词wk,统计他们在本类文章中出现的次数nk: 4. 最后应用下方的公式: 本文档使用 书栈(BookStack.CN) 构建 - 229 -

230.使用朴素贝叶斯进行分类 使用朴素贝叶斯进行分类 使用朴素贝叶斯进行分类 使用朴素贝叶斯进行分类 分类阶段比较简单,直接应用贝叶斯公式就可以了,让我们试试吧! 通过训练,我们得到以下概率结果: 比如下面这句话,要如何判断它是正面还是负面的呢? I am stunned by the hype over gravity. 本文档使用 书栈(BookStack.CN) 构建 - 230 -

231.使用朴素贝叶斯进行分类 我们需要计算的是下面两个概率,并选取较高的结果: 1. P(like)×P(I|like)×P(am|like)×P(stunned|like)×... 2. P(dislike)×P(I|dislike)×P(am|dislike)×P(stunned|dislike)×... 因此分类的结果是“讨厌”。 提示 结果中的6.22E-22是科学计数法,等价于6.22×10-22。 哇,这个概率也太小了吧! 是的,如果文本中有100个单词,那乘出来的概率就会更小。 但是Python不能处理那么小的小数,最后都会变成零的。 本文档使用 书栈(BookStack.CN) 构建 - 231 -

232.使用朴素贝叶斯进行分类 没错,因此我们要用对数来算——将每个概率的对数相加! 假设一个包含100字的文本中,每个单词的概率是0.0001,那么计算结果是: 1. >>> 0.0001 ** 100 2. 0.0 如果我们用对数相加来运算的话: 1. >>> import math 2. >>> p = 0 3. >>> for i in range(100): 4. ... p += math.log(0.0001) 5. ... 6. >>> p 7. -921.034037197617 提示 bn = x 可以转换为 logbx = n log10(ab) = log10(a) + log10(b) 本文档使用 书栈(BookStack.CN) 构建 - 232 -

233.新闻组语料库 新闻组语料库 新闻组语料库 把不要的东西丢掉! 常用词和停词 编写Python代码 新闻组语料库 我们下面要处理的数据集是新闻,这些新闻可以分为不同的新闻组,我们会构造一个分类器来判断某 则新闻是属于哪个新闻组的: 比如下面这则新闻是属于rec.motorcycles组的: 本文档使用 书栈(BookStack.CN) 构建 - 233 -

234.新闻组语料库 注意到这则新闻中还有一些拼写错误(如accesories、ussually等),这对分类器是一个不小的 挑战。 这些数据集都来自 http://qwone.com/~jason/20Newsgroups/ (我们使用的是20news- bydate数据集),你也可以从 这里 获得。 这个数据集包含18,846个文档,并将训练集(60%)和测试集放在了不同的目录中,每个子目录都是 一个新闻组,目录中的文件即新闻文本。 把不要的东西丢掉! 比如我们要对下面这篇新闻做分类: 让我们看看哪些单词是比较重要的: 本文档使用 书栈(BookStack.CN) 构建 - 234 -

235.新闻组语料库 (helpful - 重要,not helpful - 不重要) 如果我们将英语中最常用的200个单词剔除掉,这篇新闻就成了这样: 去除掉这些单词后,新闻就只剩下一半大小了。而且,这些单词看上去并不会对分类结果产生影响。 H.P. Luhn在他的论文中说“这些组成语法结构的单词是没有意义的,反而会产生很多噪音”。 也就是说,将这些“噪音”单词去除后是会提升分类正确率的。我们将这些单词称为“停词”,有专门的 停词表可供使用。去除这些词的理由是: 1. 能够减少需要处理的数据量; 本文档使用 书栈(BookStack.CN) 构建 - 235 -

236.新闻组语料库 2. 这些词的存在会对分类效果产生负面影响。 常用词和停词 虽然像the、a这种单词的确没有意义,但有些常用词如work、write、school等在某些场合下还是 有作用的,如果将他们也列进停词表里可能会有问题。 年轻人,那些常用词是不能随便丢弃的! 因此在定制停词表时还是需要做些考虑的。比如要判别阿拉伯语文档是在哪个地区书写的,可以只看 文章中最常出现的词(和上面的方式相反)。如果你有兴趣,可以到我的 个人网站 上看看这篇论 文。 而在分析聊天记录时,强奸犯会使用更多I、me、you这样的词汇,如果在分析前将这些单词去除了, 效果就会变差。 不要盲目地使用停词表! 编写Python代码 本文档使用 书栈(BookStack.CN) 构建 - 236 -

237.新闻组语料库 首先让我们实现朴素贝叶斯分类器的训练部分。训练集的格式是这样的: 最上层的目录是训练集(20news-bydate-train),其下的子目录代表不同的新闻组(如 alt.atheism),子目录中有多个文本文件,即新闻内容。测试集的目录结构也是相同的。因此,分 类器的初始化代码要完成以下工作: 1. 读取停词列表; 2. 获取训练集中各目录(分类)的名称; 3. 对于各个分类,调用train方法,统计单词出现的次数; 4. 计算下面的公式: 1. from __future__ import print_function 2. import os, codecs, math 3. 4. class BayesText: 5. 6. def __init__(self, trainingdir, stopwordlist): 7. """朴素贝叶斯分类器 8. trainingdir 训练集目录,子目录是分类,子目录中包含若干文本 9. stopwordlist 停词列表(一行一个) 10. """ 11. self.vocabulary = {} 12. self.prob = {} 13. self.totals = {} 14. self.stopwords = {} 15. f = open(stopwordlist) 16. for line in f: 17. self.stopwords[line.strip()] = 1 18. f.close() 19. categories = os.listdir(trainingdir) 本文档使用 书栈(BookStack.CN) 构建 - 237 -

238.新闻组语料库 20. # 将不是目录的元素过滤掉 21. self.categories = [filename for filename in categories 22. if os.path.isdir(trainingdir + filename)] 23. print("Counting ...") 24. for category in self.categories: 25. print(' ' + category) 26. (self.prob[category], 27. self.totals[category]) = self.train(trainingdir, category) 28. # 删除出现次数小于3次的单词 29. toDelete = [] 30. for word in self.vocabulary: 31. if self.vocabulary[word] < 3: 32. # 遍历列表时不能删除元素,因此做一个标记 33. toDelete.append(word) 34. # 删除 35. for word in toDelete: 36. del self.vocabulary[word] 37. # 计算概率 38. vocabLength = len(self.vocabulary) 39. print("Computing probabilities:") 40. for category in self.categories: 41. print(' ' + category) 42. denominator = self.totals[category] + vocabLength 43. for word in self.vocabulary: 44. if word in self.prob[category]: 45. count = self.prob[category][word] 46. else: 47. count = 1 48. self.prob[category][word] = (float(count + 1) 49. / denominator) 50. print ("DONE TRAINING\n\n") 51. 52. 53. def train(self, trainingdir, category): 54. """计算分类下各单词出现的次数""" 55. currentdir = trainingdir + category 56. files = os.listdir(currentdir) 57. counts = {} 58. total = 0 59. for file in files: 60. #print(currentdir + '/' + file) 61. f = codecs.open(currentdir + '/' + file, 'r', 'iso8859-1') 62. for line in f: 63. tokens = line.split() 64. for token in tokens: 65. # 删除标点符号,并将单词转换为小写 66. token = token.strip('\'".,?:-') 67. token = token.lower() 本文档使用 书栈(BookStack.CN) 构建 - 238 -

239.新闻组语料库 68. if token != '' and not token in self.stopwords: 69. self.vocabulary.setdefault(token, 0) 70. self.vocabulary[token] += 1 71. counts.setdefault(token, 0) 72. counts[token] += 1 73. total += 1 74. f.close() 75. return(counts, total) 训练结果存储在一个名为prop的字典里,字典的键是分类,值是另一个字典——键是单词,值是概率。 god这个词在rec.motorcycles新闻组中出现的概率是0.00013,而在 soc.religion.christian新闻组中出现的概率是0.00424。 训练阶段的另一个产物是分类列表: 本文档使用 书栈(BookStack.CN) 构建 - 239 -

240.新闻组语料库 训练结束了,下面让我们开始进行文本分类吧。 请尝试编写一个分类器,达成以下效果: 1. def classify(self, filename): 2. results = {} 3. for category in self.categories: 4. results[category] = 0 5. f = codecs.open(filename, 'r', 'iso8859-1') 6. for line in f: 7. tokens = line.split() 8. for token in tokens: 9. #print(token) 10. token = token.strip('\'".,?:-').lower() 11. if token in self.vocabulary: 12. for category in self.categories: 13. if self.prob[category][token] == 0: 14. print("%s %s" % (category, token)) 15. results[category] += math.log( 16. self.prob[category][token]) 17. f.close() 18. results = list(results.items()) 19. results.sort(key=lambda tuple: tuple[1], reverse = True) 本文档使用 书栈(BookStack.CN) 构建 - 240 -

241.新闻组语料库 20. # 如果要调试,可以打印出整个列表。 21. return results[0][0] 最后我们编写一个函数对测试集中的所有文档进行分类,并计算准确率: 1. def testCategory(self, directory, category): 2. files = os.listdir(directory) 3. total = 0 4. correct = 0 5. for file in files: 6. total += 1 7. result = self.classify(directory + file) 8. if result == category: 9. correct += 1 10. return (correct, total) 11. 12. def test(self, testdir): 13. """测试集的目录结构和训练集相同""" 14. categories = os.listdir(testdir) 15. # 过滤掉不是目录的元素 16. categories = [filename for filename in categories if 17. os.path.isdir(testdir + filename)] 18. correct = 0 19. total = 0 20. for category in categories: 21. print(".", end="") 22. (catCorrect, catTotal) = self.testCategory( 23. testdir + category + '/', category) 24. correct += catCorrect 25. total += catTotal 26. print("\n\nAccuracy is %f%% (%i test instances)" % 27. ((float(correct) / total) * 100, total)) 在不使用停词列表的情况下,这个分类器的效果是: 本文档使用 书栈(BookStack.CN) 构建 - 241 -

242.新闻组语料库 准确率77.77%,看起来很不错。如果用了停词列表效果会如何呢? 那让我们来测试一下吧! 请自行到网络上查找一些停词列表,并填写以下表格: 我找到了两个停词列表,分别是包含25个词 和174个词 的列表,结果如下: 看来第二个停词列表能提升2%的效果,你的结果如何? 本文档使用 书栈(BookStack.CN) 构建 - 242 -

243.新闻组语料库 本文档使用 书栈(BookStack.CN) 构建 - 243 -

244.朴素贝叶斯与情感分析 朴素贝叶斯与情感分析 朴素贝叶斯与情感分析 朴素贝叶斯与情感分析 情感分析的目的是判断作者的态度或意见: 情感分析的例子之一是判断一篇评论是正面的还是反面的,我们可以用朴素贝叶斯算法来实现。 我们可以用Pang&Lee 2004的影评数据来测试,这份数据集包含1000个正面和1000个负面的评价, 以下是一些示例: 本月第二部连环杀人犯电影实在太糟糕了!虽然开头的故事情节和场景布置还可以,但后面就…… 当我听说罗密欧与朱丽叶又出了一部改编电影后,心想莎士比亚的经典又要被糟蹋了。不过我错了,Baz Luhrman导演的水平还是高 的…… 你可以从 http://www.cs.cornell.edu/People/pabo/movie-review-data/ 上下载这个数 据集,并整理成以下形式: 本文档使用 书栈(BookStack.CN) 构建 - 244 -

245.朴素贝叶斯与情感分析 你也可以从这里下载整理好的数据。 动手实践 你可以为上文的朴素贝叶斯分类器增加十折交叉验证的逻辑吗?它的输出结果应该是如下形式: 另外,请计算Kappa指标。 再次声明:只看不练是不行的,就好比你不可能通过阅读乐谱就学会弹奏钢琴。 解答 本文档使用 书栈(BookStack.CN) 构建 - 245 -

246.朴素贝叶斯与情感分析 这是我得到的结果: Kappa指标则是: 所以我们的分类算法效果是不错的。 代码链接 本文档使用 书栈(BookStack.CN) 构建 - 246 -

247.聚类 聚类 第八章:聚类 第八章:聚类 原文:http://guidetodatamining.com/chapter8/ 内容: 层次聚类法 编写层次聚类算法 k-means聚类算法 安然事件 前几章我们学习了如何构建分类系统,使用的是已经标记好类别的数据集进行训练: 训练完成后我们就可以用来预测了:这个人看起来像是篮球运动员,那个人可能是练体操的;这个人 三年内不会患有糖尿病。 可以看到,分类器在训练阶段就已经知道各个类别的名称了。那如果我们不知道呢?如何构建一个能 够自动对数据进行分组的系统?比如有1000人,每人有20个特征,我想把这些人分为若干个组。 本文档使用 书栈(BookStack.CN) 构建 - 247 -

248.聚类 这个过程叫做聚类:通过物品特征来计算距离,并自动分类到不同的群集或组中。有两种聚类算法比 较常用: k-means聚类算法 我们会事先告诉这个算法要将数据分成几个组,比如“请把这1000个人分成5个组”,“将这些网页分 成15个组”。这种方法就叫k-means,我们会在后面的章节讨论。 本文档使用 书栈(BookStack.CN) 构建 - 248 -

249.层次聚类法 层次聚类法 层次聚类法 单链聚类 全链聚类 平均链接聚类 层次聚类法 对于层次聚类法,我们不需要预先指定分类的数量,这个算方法会将每条数据都当作是一个分类,每 次迭代的时候合并距离最近的两个分类,直到剩下一个分类为止。 因此聚类的结果是:顶层有一个大分类,这个分类下有两个子分类,每个子分类下又有两个子分类, 依此类推,层次聚类也因此得命。 本文档使用 书栈(BookStack.CN) 构建 - 249 -

250.层次聚类法 在合并的时候我们会计算两个分类之间的距离,可以采用不同的方法。如下图中的A、B、C三个分 类,我们应该将哪两个分类合并起来呢? 本文档使用 书栈(BookStack.CN) 构建 - 250 -

251.层次聚类法 单链聚类 在单链聚类中,分类之间的距离由两个分类相距最近的两个元素决定。如上图中分类A和分类B的距离 由A1和B1的距离决定,因为这个距离小于A1到B2、A2到B1的距离。这样一来我们会将A和B进行合 并。 全链聚类 在全链聚类中,分类之间的距离由两个分类相距最远的两个元素决定。因此上图中分类A和B的距离是 A2到B2的距离,最后会将分类B和C进行合并。 平均链接聚类 在这种聚类方法中,我们通过计算分类之间两两元素的平均距离来判断分类之间的距离,因此上图中 会将分类B和C进行合并。 下面让我们用单链聚类法举个例子吧! 我们来用狗的高度和重量来进行聚类: 本文档使用 书栈(BookStack.CN) 构建 - 251 -

252.层次聚类法 在计算距离前我们是不是忘了做件事? 本文档使用 书栈(BookStack.CN) 构建 - 252 -

253.层次聚类法 标准化!我们先将这些数据转换为修正的标准分。 然后我们计算欧几里德距离,图中高亮了一些最短距离: 本文档使用 书栈(BookStack.CN) 构建 - 253 -

254.层次聚类法 根据下面的图表,你能看出哪两个品种的距离最近吗? 如果你看出是Border Collie和Portuguese Water Dog最近,那就对了! 计算过程 第一步:我们找到距离最近的两个元素,对他们进行聚类: 本文档使用 书栈(BookStack.CN) 构建 - 254 -

255.层次聚类法 第二步:再找出距离最近的两个元素,进行聚类: 第三步:继续重复上面的步骤: 第四步:继续查找距离最近的元素,发现Border Collie已经属于一个分类的,因此进行如下图所 示的合并: 这叫树状图,可以用来表示聚类。 动手实践 本文档使用 书栈(BookStack.CN) 构建 - 255 -

256.层次聚类法 你能在下图的基础上继续完成聚类吗? 解答 本文档使用 书栈(BookStack.CN) 构建 - 256 -

257.层次聚类法 本文档使用 书栈(BookStack.CN) 构建 - 257 -

258.编写层次聚类算法 编写层次聚类算法 编写层次聚类算法 从文件中读取数据 初始化优先队列 距离相等的问题以及为何要使用元组 距离相等的另一个问题 重复下述步骤,直到仅剩一个分类 编写层次聚类算法 我们可以使用优先队列来实现这个聚类算法。 什么是优先队列呢? 普通的队列有“先进先出”的规则,比如向队列先后添加Moa、Suzuka、Yui,取出时得到的也是 Moa、Suzuka、Yui: 而对于优先队列,每个元素都可以附加一个优先级,从队列中取出时会得到优先级最高的元素。比如 说,我们定义年龄越小优先级越高,以下是插入过程: 本文档使用 书栈(BookStack.CN) 构建 - 258 -

259.编写层次聚类算法 取出的第一个元素是Yui,因为她的年龄最小: 我们看看Python中如何使用优先队列: 1. >>> from Queue import PriorityQueue # 加载优先队列类 2. >>> singersQueue = PriorityQueue() # 创建对象 3. >>> singersQueue.put((16, 'Suzuka Nakamoto')) # 插入元素 4. >>> singersQueue.put((15, 'Moa Kikuchi')) 5. >>> singersQueue.put((14, 'Yui Mizuno')) 6. >>> singersQueue.put((17, 'Ayaka Sasaki')) 7. >>> singersQueue.get() # 获取第一个元素,即最年轻的歌手Yui。 8. (14, 'Yui Mizuno') 9. >>> singersQueue.get() 10. (15, 'Moa Kikuchi') 11. >>> singersQueue.get() 12. (16, 'Suzuka Nakamoto') 13. >>> singersQueue.get() 14. (17, 'Ayaka Sasaki') 在进行聚类时,我们将分类、离它最近的分类、以及距离插入到优先队列中,距离作为优先级。比如 上面的犬种示例,Border Collie最近的分类是Portuguese WD,距离是0.232: 本文档使用 书栈(BookStack.CN) 构建 - 259 -

260.编写层次聚类算法 我们将优先队列中距离最小的两个分类取出来,合并成一个分类,并重新插入到优先队列中。比如下 图是将Border Collie和Portuguese WD合并后的结果: 重复这个过程,直到队列中只有一个元素为止。当然,我们插入的数据会复杂一些,请看下面的讲 解。 从文件中读取数据 数据文件是CSV格式的(以逗号分隔),第一行是列名,第一列是犬种,第二列之后是特征值: 本文档使用 书栈(BookStack.CN) 构建 - 260 -

261.编写层次聚类算法 我们用Python的列表结构来存储这些数据,data[0]用来存放所有记录的分类,如data[0][0]是 Border Collie,data[0][1]是Boston Terrier。data[1]则是所有记录的高度,data[2]是 重量。 特征列的数据都会转换成浮点类型,如data[1][0]是20.0,data[2][0]是45.0等。在读取数据时 就需要对其进行标准化。此外,我们接下来会使用“下标”这个术语,如第一条记录Border Collie 的下标是0,第二条记录Boston Terrier下标是1等。 初始化优先队列 以Border Collie为例,我们需要计算它和其它犬种的距离,保存在Python字典里: 1. {1: ((0, 1), 1.0244), # Border Collie(下标为0)和Boston Terrier(下标为1)之间的距离为1.0244 2. 2: ((0, 2), 0.463), # Border Collie和Brittany Spaniel(下标为2)之间的距离为0.463 3. ... 4. 10: ((0, 10), 2.756)} # Border Collie和Yorkshire Terrier的距离为2.756 此外,我们会记录Border Collie最近的分类及距离:这对犬种是(0, 8),即下标为0的Border Collie和下标为8的Portuguese WD,距离是0.232。 距离相等的问题以及为何要使用元组 你也许注意到了,Portuguese WD和Standard Poodle的距离是0.566,Boston Terrier和 Brittany Spaniel的距离也是0.566, 如果我们通过最短距离来取,很可能会取出Standard Poodle和Boston Terrier进行组合,这显 然是错误的,所以我们才会使用元组来存放这对犬种的下标,以作判断。比如说,Portuguese WD 的记录是: 1. ['Portuguese Water Dog', 0.566, (8, 9)] 本文档使用 书栈(BookStack.CN) 构建 - 261 -

262.编写层次聚类算法 它的近邻Standard Poodle的记录是: 1. ['Standard Poodle', 0.566, (8, 9)] 我们可以通过这个元组来判断这两条记录是否是一对。 距离相等的另一个问题 在介绍优先队列时,我用了歌手的年龄举例,如果他们的年龄相等,取出的顺序又是怎样的呢? 可以看到,如果年龄相等,优先队列会根据记录中的第二个元素进行判断,即歌手的姓名,并按字母 顺序返回,如Avaka会比Moa优先返回。 在犬种示例中,我们让距离成为第一优先级,下标成为第二优先级。因此,我们插入到优先队列的一 条完整记录是这样的: 本文档使用 书栈(BookStack.CN) 构建 - 262 -

263.编写层次聚类算法 重复下述步骤,直到仅剩一个分类 我们从优先队列中取出两个元素,对它们进行合并。如合并Border Collie和Portuguese WD后, 会形成一个新的分类: 1. ['Border Collie', 'Portuguese WD'] 然后我们需要计算新的分类和其它分类之间的距离,方法是对取出的两个分类的距离字典进行合并。 如第一个分类的距离字段是distanceDict1,第二个分类的是distanceDict2,新的距离字段是 newDistanceDict: 1. 初始化newDistanceDict 2. 对于distanceDict1的每一个键值对: 3. 如果这个键在distanceDict2中存在: 4. 如果这个键在distanceDict1中的距离要比在distanceDict2中的距离小: 5. 将distanceDict1中的距离存入newDistanceDict 6. 否则: 7. 将distanceDict2中的距离存入newDistanceDict 本文档使用 书栈(BookStack.CN) 构建 - 263 -

264.编写层次聚类算法 经过计算后,插入到优先队列中的新分类的完整记录是: 代码实践 你能将上面的算法用Python实现吗?你可以从hierarchicalClustererTemplate.py这个文件开 始,完成以下步骤: 1. 编写init方法,对于每条记录: i. 计算该分类和其它分类之间的欧几里得距离; ii. 找出该分类的近邻; iii. 将这些信息放到优先队列的中。 2. 编写cluster方法,重复以下步骤,直至剩下一个分类: i. 从优先队列中获取两个元素; ii. 合并; iii. 将合并后的分类放回优先队列中。 本文档使用 书栈(BookStack.CN) 构建 - 264 -

265.编写层次聚类算法 解答 注意,我的实现并不一定是最好的,你可以写出更好的! 1. from queue import PriorityQueue 2. import math 3. 4. """ 5. 层次聚类示例代码 6. """ 7. 8. def getMedian(alist): 9. """计算中位数""" 10. tmp = list(alist) 11. tmp.sort() 12. alen = len(tmp) 13. if (alen % 2) == 1: 14. return tmp[alen // 2] 15. else: 16. return (tmp[alen // 2] + tmp[(alen // 2) - 1]) / 2 17. 18. def normalizeColumn(column): 19. """计算修正的标准分""" 20. median = getMedian(column) 21. asd = sum([abs(x - median) for x in column]) / len(column) 22. result = [(x - median) / asd for x in column] 23. return result 24. 25. class hClusterer: 26. """该聚类器默认数据的第一列是标签,其它列是数值型的特征。""" 27. 28. def __init__(self, filename): 29. file = open(filename) 本文档使用 书栈(BookStack.CN) 构建 - 265 -

266.编写层次聚类算法 30. self.data = {} 31. self.counter = 0 32. self.queue = PriorityQueue() 33. lines = file.readlines() 34. file.close() 35. header = lines[0].split(',') 36. self.cols = len(header) 37. self.data = [[] for i in range(len(header))] 38. for line in lines[1:]: 39. cells = line.split(',') 40. toggle = 0 41. for cell in range(self.cols): 42. if toggle == 0: 43. self.data[cell].append(cells[cell]) 44. toggle = 1 45. else: 46. self.data[cell].append(float(cells[cell])) 47. # 标准化特征列(即跳过第一列) 48. for i in range(1, self.cols): 49. self.data[i] = normalizeColumn(self.data[i]) 50. 51. ### 52. ### 数据已经读入内存并做了标准化,对于每一条记录,将执行以下步骤: 53. ### 1. 计算该分类和其他分类的距离,如当前分类的下标是1, 54. ### 它和下标为2及下标为3的分类之间的距离用以下形式表示: 55. ### {2: ((1, 2), 1.23), 3: ((1, 3), 2.3)... } 56. ### 2. 找出距离最近的分类; 57. ### 3. 将该分类插入到优先队列中。 58. ### 59. 60. # 插入队列 61. rows = len(self.data[0]) 62. 63. for i in range(rows): 64. minDistance = 99999 65. nearestNeighbor = 0 66. neighbors = {} 67. for j in range(rows): 68. if i != j: 69. dist = self.distance(i, j) 70. if i < j: 71. pair = (i,j) 72. else: 73. pair = (j,i) 74. neighbors[j] = (pair, dist) 75. if dist < minDistance: 76. minDistance = dist 77. nearestNeighbor = j 本文档使用 书栈(BookStack.CN) 构建 - 266 -

267.编写层次聚类算法 78. nearestNum = j 79. # 记录这两个分类的配对信息 80. if i < nearestNeighbor: 81. nearestPair = (i, nearestNeighbor) 82. else: 83. nearestPair = (nearestNeighbor, i) 84. 85. # 插入优先队列 86. self.queue.put((minDistance, self.counter, 87. [[self.data[0][i]], nearestPair, neighbors])) 88. self.counter += 1 89. 90. def distance(self, i, j): 91. sumSquares = 0 92. for k in range(1, self.cols): 93. sumSquares += (self.data[k][i] - self.data[k][j])**2 94. return math.sqrt(sumSquares) 95. 96. def cluster(self): 97. done = False 98. while not done: 99. topOne = self.queue.get() 100. nearestPair = topOne[2][1] 101. if not self.queue.empty(): 102. nextOne = self.queue.get() 103. nearPair = nextOne[2][1] 104. tmp = [] 105. 106. ## 我从队列中取出了两个元素:topOne和nextOne, 107. ## 检查这两个分类是否是一对,如果不是就继续从优先队列中取出元素, 108. ## 直至找到topOne的配对分类为止。 109. while nearPair != nearestPair: 110. tmp.append((nextOne[0], self.counter, nextOne[2])) 111. self.counter += 1 112. nextOne = self.queue.get() 113. nearPair = nextOne[2][1] 114. 115. ## 将不处理的元素退回给优先队列 116. for item in tmp: 117. self.queue.put(item) 118. 119. if len(topOne[2][0]) == 1: 120. item1 = topOne[2][0][0] 121. else: 122. item1 = topOne[2][0] 123. if len(nextOne[2][0]) == 1: 124. item2 = nextOne[2][0][0] 125. else: 本文档使用 书栈(BookStack.CN) 构建 - 267 -

268.编写层次聚类算法 126. item2 = nextOne[2][0] 127. ## curCluster即合并后的分类 128. curCluster = (item1, item2) 129. 130. ## 对于这个新的分类需要做两件事情:首先找到离它最近的分类,然后合并距离字典。 131. ## 如果item1和元素23的距离是2,item2和元素23的距离是4,我们取较小的那个距离,即单链聚类。 132. minDistance = 99999 133. nearestPair = () 134. nearestNeighbor = '' 135. merged = {} 136. nNeighbors = nextOne[2][2] 137. for (key, value) in topOne[2][2].items(): 138. if key in nNeighbors: 139. if nNeighbors[key][1] < value[1]: 140. dist = nNeighbors[key] 141. else: 142. dist = value 143. if dist[1] < minDistance: 144. minDistance = dist[1] 145. nearestPair = dist[0] 146. nearestNeighbor = key 147. merged[key] = dist 148. 149. if merged == {}: 150. return curCluster 151. else: 152. self.queue.put( (minDistance, self.counter, 153. [curCluster, nearestPair, merged])) 154. self.counter += 1 155. 156. def printDendrogram(T, sep=3): 157. """打印二叉树状图。树的每个节点是一个二元组。这个方法摘自: 158. http://code.activestate.com/recipes/139422-dendrogram-drawing/""" 159. 160. def isPair(T): 161. return type(T) == tuple and len(T) == 2 162. 163. def maxHeight(T): 164. if isPair(T): 165. h = max(maxHeight(T[0]), maxHeight(T[1])) 166. else: 167. h = len(str(T)) 168. return h + sep 169. 170. activeLevels = {} 171. 172. def traverse(T, h, isFirst): 173. if isPair(T): 本文档使用 书栈(BookStack.CN) 构建 - 268 -

269.编写层次聚类算法 174. traverse(T[0], h-sep, 1) 175. s = [' ']*(h-sep) 176. s.append('|') 177. else: 178. s = list(str(T)) 179. s.append(' ') 180. 181. while len(s) < h: 182. s.append('-') 183. 184. if (isFirst >= 0): 185. s.append('+') 186. if isFirst: 187. activeLevels[h] = 1 188. else: 189. del activeLevels[h] 190. 191. A = list(activeLevels) 192. A.sort() 193. for L in A: 194. if len(s) < L: 195. while len(s) < L: 196. s.append(' ') 197. s.append('|') 198. 199. print (''.join(s)) 200. 201. if isPair(T): 202. traverse(T[1], h-sep, 0) 203. 204. traverse(T, maxHeight(T), -1) 205. 206. 207. filename = '/Users/raz/Dropbox/guide/data/dogs.csv' 208. 209. hg = hClusterer(filename) 210. cluster = hg.cluster() 211. printDendrogram(cluster) 运行结果和我们手算的一致: 本文档使用 书栈(BookStack.CN) 构建 - 269 -

270.编写层次聚类算法 动手实践 这里提供了77种早餐麦片的营养信息,包括以下几项: 麦片名称 热量 蛋白质 脂肪 纳 纤维 碳水化合物 糖 钾 维生素 请对这个数据集进行层次聚类: 哪种麦片和Trix最相近? 本文档使用 书栈(BookStack.CN) 构建 - 270 -

271.编写层次聚类算法 与Muesli Raisins & Almonds最相近的是? 数据集来自:http://lib.stat.cmu.edu/DASL/Datafiles/Cereals.html 结果 我们只需将代码中的文件名替换掉就可以了,结果如下: 因此Trix和Fruity Pebbles最相似(你可以去买这两种麦片尝尝)。Muesli Raisins & Almonds和Muesli Peaches & Pecans最相似。 好了,这就是层次聚类算法,很简单吧! 本文档使用 书栈(BookStack.CN) 构建 - 271 -

272.k-means聚类算法 k-means聚类算法 k-means聚类算法 登山式算法 误差平方和(SSE) k-means++ k-means聚类算法 使用k-means算法时需要指定分类的数量,这也是算法名称中“k”的由来。 k-means是Lloyd博士在1957年提出的,虽然这个算法已有50年的历史,但却是当前最流行的聚类算法! 下面让我们来了解一下k-means聚类过程: 本文档使用 书栈(BookStack.CN) 构建 - 272 -

273.k-means聚类算法 1. 我们想将图中的记录分成三个分类(即k=3),比如上文提到的犬种数据,坐标轴分别是身高和 体重。 2. 由于k=3,我们随机选取三个点来作为聚类的起始点(分类的中心点),并用红黄蓝三种颜色标 识。 3. 然后,我们根据其它点到中心点的距离来进行分配,这样就能将这些点分成三类了。 4. 计算这些分类的中心点,以此作为下一次计算的起始点。重复这个过程,直到中心点不再变动, 或迭代次数超过某个阈值为止。 所以k-means算法可概括为: 1. 随机选取k个元素作为中心点; 2. 根据距离将各个点分配给中心点; 3. 计算新的中心点; 4. 重复2、3,直至满足条件。 我们来看一个示例,将以下点分成两个分类: 第一步 随机选取中心点 本文档使用 书栈(BookStack.CN) 构建 - 273 -

274.k-means聚类算法 我们选取(1, 4)作为分类1的中心点,(4, 2)作为分类2的中心点; 第二步 将各点分配给中心点 可以用各类距离计算公式,为简单起见,这里我们使用曼哈顿距离: 聚类结果如下: 第三步 更新中心点 通过计算平均值来更新中心点,如x轴的均值是: (1 + 1 + 2) / 3 = 4 / 3 = 1.33 y轴是: (2 + 4 + 3) / 3 = 9 / 3 = 3 因此分类1的中心点是(1.33, 3)。计算得到分类2的中心点是(4, 2.4)。 第四步 重复前面两步 两个分类的中心点由(1, 4)、(4, 2)变为了(1.33, 3)、(4, 2.4),我们使用新的中心点重新计 算。 重复第二步 将各点分配给中心点 本文档使用 书栈(BookStack.CN) 构建 - 274 -

275.k-means聚类算法 同样是计算曼哈顿距离: 新的聚类结果是: 重复第三步 更新中心点 分类1:(1.5, 2.75) 分类2:(4.5, 2.5) 重复第二步 将各点分配给中心点 本文档使用 书栈(BookStack.CN) 构建 - 275 -

276.k-means聚类算法 重复第三步 更新中心点 分类1:(1.5, 2.75) 分类2:(4.5, 2.5) 可以看到中心点并没有改变,所以计算也就结束了。 当中心点不再变化时,或者说不再有某个点从一个分类转移到另一个分类时,我们就会停止计算。这个时候我们称该算法已经收敛。 算法运行过程中,中心点的大幅转移是在前几次迭代中产生的,后面的迭代中变动的幅度就会减小。也就是说,k-means算法的重点 是在前期迭代,而后期的迭代只是细微的调整。 基于k-means的这种特点,我们可以将“没有点发生转移”弱化成“少于1%的点发生转移”来作为计算停止条件,这也是最普遍的做法。 本文档使用 书栈(BookStack.CN) 构建 - 276 -

277.k-means聚类算法 k-means好简单呀! 扩展阅读 k-means是一种最大期望算法,这类算法会在“期望”和“最大化”两个阶段不断迭代。比如k-means 的期望阶段是将各个点分配到它们所“期望”的分类中,然后在最大化阶段重新计算中心点的位置。如 果你对此感兴趣,可以前去阅读维基百科上的词条。 登山式算法 再继续讨论k-means算法之前,我想先介绍一下登山式算法。 假设我们想要登上一座山的顶峰,可以通过以下步骤实现: 1. 在山上随机选取一个点作为开始; 2. 向高处爬一点; 3. 重复第2步,直到没有更高的点。 这种做法看起来很合理,比如对于下图所示的山峰: 无论我们从哪个点开始攀登,最终都可以达到顶峰。 但对于下面这张图: 本文档使用 书栈(BookStack.CN) 构建 - 277 -

278.k-means聚类算法 所以说,这种简单的登山式算法并不一定能得到最优解。 k-means就是这样一种算法,它不能保证最终结果是最优的,因为我们一开始选择的中心点是随机 的,很有可能就会选到上面的A点,最终获得局部最优解B点。因此,最终的聚类结果和起始点的选择 有很大关系。但尽管如此,k-means通常还是能够获得良好的结果的。 那我们如何比较不同的聚类结果呢? 误差平方和(SSE) 我们可以使用误差平方和(或称离散程度)来评判聚类结果的好坏,它的计算方法是:计算每个点到 中心点的距离平方和。 本文档使用 书栈(BookStack.CN) 构建 - 278 -

279.k-means聚类算法 上面的公式中,第一个求和符号是遍历所有的分类,比如i=1时计算第一个分类,i=2时计算第二个分 类,直到计算第k个分类;第二个求和符号是遍历分类中所有的点;Dist指代距离计算公式(如曼哈 顿距离、欧几里得距离);计算数据点x和中心点ci之间的距离,平方后相加。 假设我们对同一数据集使用了两次k-means聚类,每次选取的起始点不一样,想知道最后得到的聚类 结果哪个更优,就可以计算和比较SSE,结果小的效果好。 下面让我们开始编程吧! 1. import math 2. import random 3. 4. 5. """ 6. K-means算法 7. """ 8. 9. def getMedian(alist): 10. """计算中位数""" 11. tmp = list(alist) 12. tmp.sort() 13. alen = len(tmp) 14. if (alen % 2) == 1: 15. return tmp[alen // 2] 16. else: 17. return (tmp[alen // 2] + tmp[(alen // 2) - 1]) / 2 18. 19. 20. def normalizeColumn(column): 21. """计算修正的标准分""" 22. median = getMedian(column) 23. asd = sum([abs(x - median) for x in column]) / len(column) 本文档使用 书栈(BookStack.CN) 构建 - 279 -

280.k-means聚类算法 24. result = [(x - median) / asd for x in column] 25. return result 26. 27. 28. class kClusterer: 29. """kMeans聚类算法,第一列是分类,其余列是数值型特征""" 30. 31. def __init__(self, filename, k): 32. """k是分类的数量,该函数完成以下功能: 33. 1. 读取filename的文件内容 34. 2. 按列存储到self.data变量中 35. 3. 计算修正的标准分 36. 4. 随机选取起始点 37. 5. 将各个点分配给中心点 38. """ 39. file = open(filename) 40. self.data = {} 41. self.k = k 42. self.counter = 0 43. self.iterationNumber = 0 44. # 用于跟踪本次迭代有多少点的分类发生了变动 45. self.pointsChanged = 0 46. # 误差平方和 47. self.sse = 0 48. # 49. # 读取文件 50. # 51. lines = file.readlines() 52. file.close() 53. header = lines[0].split(',') 54. self.cols = len(header) 55. self.data = [[] for i in range(len(header))] 56. # 按列存储数据,如self.data[0]是第一列的数据, 57. # self.data[0][10]是第一列第十行的数据。 58. for line in lines[1:]: 59. cells = line.split(',') 60. toggle = 0 61. for cell in range(self.cols): 62. if toggle == 0: 63. self.data[cell].append(cells[cell]) 64. toggle = 1 65. else: 66. self.data[cell].append(float(cells[cell])) 67. 68. self.datasize = len(self.data[1]) 69. self.memberOf = [-1 for x in range(len(self.data[1]))] 70. # 71. # 标准化 本文档使用 书栈(BookStack.CN) 构建 - 280 -

281.k-means聚类算法 72. # 73. for i in range(1, self.cols): 74. self.data[i] = normalizeColumn(self.data[i]) 75. 76. # 随机选取起始点 77. random.seed() 78. self.centroids = [[self.data[i][r] for i in range(1, len(self.data))] 79. for r in random.sample(range(len(self.data[0])), 80. self.k)] 81. self.assignPointsToCluster() 82. 83. 84. 85. def updateCentroids(self): 86. """根据分配结果重新确定聚类中心点""" 87. members = [self.memberOf.count(i) for i in range(len(self.centroids))] 88. self.centroids = [[sum([self.data[k][i] 89. for i in range(len(self.data[0])) 90. if self.memberOf[i] == centroid])/members[centroid] 91. for k in range(1, len(self.data))] 92. for centroid in range(len(self.centroids))] 93. 94. 95. 96. def assignPointToCluster(self, i): 97. """根据距离计算所属中心点""" 98. min = 999999 99. clusterNum = -1 100. for centroid in range(self.k): 101. dist = self.euclideanDistance(i, centroid) 102. if dist < min: 103. min = dist 104. clusterNum = centroid 105. # 跟踪变动的点 106. if clusterNum != self.memberOf[i]: 107. self.pointsChanged += 1 108. # 计算距离平方和 109. self.sse += min**2 110. return clusterNum 111. 112. def assignPointsToCluster(self): 113. """分配所有的点""" 114. self.pointsChanged = 0 115. self.sse = 0 116. self.memberOf = [self.assignPointToCluster(i) 117. for i in range(len(self.data[1]))] 118. 119. 本文档使用 书栈(BookStack.CN) 构建 - 281 -

282.k-means聚类算法 120. 121. def euclideanDistance(self, i, j): 122. """计算欧几里得距离""" 123. sumSquares = 0 124. for k in range(1, self.cols): 125. sumSquares += (self.data[k][i] - self.centroids[j][k-1])**2 126. return math.sqrt(sumSquares) 127. 128. def kCluster(self): 129. """开始进行聚类,重复以下步骤: 130. 1. 更新中心点 131. 2. 重新分配 132. 直至变动的点少于1%。 133. """ 134. done = False 135. 136. while not done: 137. self.iterationNumber += 1 138. self.updateCentroids() 139. self.assignPointsToCluster() 140. # 141. # 如果变动的点少于1%则停止迭代 142. # 143. if float(self.pointsChanged) / len(self.memberOf) < 0.01: 144. done = True 145. print("Final SSE: %f" % self.sse) 146. 147. def showMembers(self): 148. """输出结果""" 149. for centroid in range(len(self.centroids)): 150. print ("\n\nClass %i\n========" % centroid) 151. for name in [self.data[0][i] for i in range(len(self.data[0])) 152. if self.memberOf[i] == centroid]: 153. print (name) 154. 155. ## 156. ## 对犬种数据进行聚类,令k=3 157. ### 158. # 请自行修改文件路径 159. km = kClusterer('../../data/dogs.csv', 3) 160. km.kCluster() 161. km.showMembers() 本文档使用 书栈(BookStack.CN) 构建 - 282 -

283.k-means聚类算法 我们来分析一下这段代码。 犬种数据用表格来展示是这样的,身高和体重都做了标准化: 因为需要按列存储,转化后的Python格式是这样的: 1. data = [['Border Collie', 'Bosten Terrier', 'Brittany Spaniel', ...], 2. [0, -0.7213, -0.3607, ...], 3. [-0.1455, -0.7213, -0.4365, ...], 4. ...] 我们在层次聚类中用的也是此法,这样做的好处是能够方便地应用不同的数学函数。比如计算中位数 和计算标准分的函数,都是以列表作为参数的: 1. >>> normalizeColumn([8, 6, 4, 2]) 2. [1.5, 0.5, -0.5, -1.5] __init__ 函数首先将文件读入进来,按列存储,并进行标准化。随后,它会选取k个起始点,并将 记录中的点分配给这些中心点。 kCluster 函数则开始迭代计算中心点的新位置,直到少于1%的点发 生变动为止。 本文档使用 书栈(BookStack.CN) 构建 - 283 -

284.k-means聚类算法 程序的运行结果如下: 1. Final SSE: 5.243159 2. 3. Class 0 4. ======= 5. Bullmastiff 6. Great Dane 7. 8. Class 1 9. ======= 10. Boston Terrier 11. Chihuahua 12. Yorkshire Terrier 13. 14. Class 2 15. ======= 16. Border Collie 17. Brittany Spaniel 18. German Shepherd 19. Golden Retriever 20. Portuguese Water Dog 21. Standard Poodle 聚类结果非常棒! 动手实践 用上面的聚类程序来对麦片数据集进行聚类,令k=4,并回答以下问题: 1. 甜味麦片都被聚类到一起了吗,如Cap’n’Crunch, Cocoa Puffs, Froot Loops, Lucky Charms? 2. 麸类麦片聚到一起了吗,如100% Bran, All-Bran, All-Bran with Extra Fiber, Bran Chex? 3. Cheerios被分到了哪个类别,是不是一直和Special K一起? 再来对加仑公里数的数据进行聚类,令k=8。运行结果大致令人满意,但有时候会出现记录数为空的 分类。 本文档使用 书栈(BookStack.CN) 构建 - 284 -

285.k-means聚类算法 我要求聚类成8个分类,但其中一个是空的,肯定代码有问题! 我们用示例来看这个问题,假设需要将以下8个点分成3个类别: 我们选取1、7、8作为起始点,因此第一次聚类的结果是: 随后,我们重新计算中心点,即下图中的加号: 这时,6离蓝色中心点较近,7离绿色中心点较近,因此粉色的分类就为空了。 所以说,虽然我们指定了k的值,但不代表最终结果就会有k个分类。这通常是好事,比如上面的例子 中,看起来就应该要分成两类。如果有1000条数据,我们指定k=10,但结果有两个为空,那很有可 能这个数据集本来就该分成8个类别,因此可以尝试用k=8来重新计算。 另一方面,如果你要求分类都不为空,那就需要改变一下算法:当发现空的分类时,就重新指定这个 分类的中心点。一种做法是选取离这个中心点最远的点,比如上面的例子中,发现粉色分类为空,就 本文档使用 书栈(BookStack.CN) 构建 - 285 -

286.k-means聚类算法 将中心点变为点1,因为它离粉色中心点最远。 k-means++ 前面我们提到k-means是50年代发明的算法,它的实现并不复杂,但仍是现今最流行的聚类算法。不 过它也有一个明显的缺点。在算法一开始需要随机选取k个起始点,正是这个随机会有问题。 有时选取的点能产生最佳结果,而有时会让结果变得很差。k-means++则改进了起始点的选取过程, 其余的和k-means一致。 以下是k-means++选取起始点的过程: 1. 随机选取一个点; 2. 重复以下步骤,直到选完k个点: i. 计算每个数据点(dp)到各个中心点的距离(D),选取最小的值,记为D(dp); ii. 根据D(dp)的概率来随机选取一个点作为中心点。 我们来讲解一下何为“根据D(dp)的概率来随机选取”。假设选取过程进行到一半,已经选出了两个 点,现在需要选第三个。假设还有五个点可供选择,它们离已有的两个中心点的距离是: 本文档使用 书栈(BookStack.CN) 构建 - 286 -

287.k-means聚类算法 Dc1表示到中心点1的距离,Dc2表示到中心点2的距离。 我们选取最小的距离: 然后将这些数值转化成总和为1的权重值,做法就是将每个距离除以距离的和(20),得到: 我们可以通过转盘游戏来理解: 本文档使用 书栈(BookStack.CN) 构建 - 287 -

288.k-means聚类算法 比如我们扔个球到这个转盘里,它停在哪个颜色就选取这个点作为新的中心点。这就叫做“根据D(dp) 的概率来随机选取”。 比如我们有以下Python数据: 1. data = [('dp1', 0.25), ('dp2', 0.4), ('dp3', 0.1), 2. ('dp4', 0.15), ('dp5', 0.1)] 然后来编写一个函数来完成选取过程: 1. import random 2. random.seed() 3. 4. def roulette(datalist): 5. i = 0 6. soFar = datalist[0][1] 7. ball = random.random() 8. while soFar < ball: 9. i += 1 10. soFar += datalist[i][1] 11. return datalist[i][0] 如果这个函数运行正确,我们选取100次的话,其中25次应该是dp1,40次是dp2,10次是dp3,15 次是dp4,10次是dp5。让我们来测试一下: 1. import collections 2. results = collections.defaultdict(int) 3. for i in range(100): 4. results[roulette(data)] += 1 5. print results 6. 7. {'dp5': 11, 'dp4': 15, 'dp3': 10, 'dp2': 38, 'dp1': 26} 本文档使用 书栈(BookStack.CN) 构建 - 288 -

289.k-means聚类算法 结果是符合预期的! k-means++选取起始点的方法总结下来就是:第一个点还是随机的,但后续的点就会尽量选择离现有 中心点更远的点。 好了,下面让我们开始写代码吧! 代码实践 你能用Python实现k-means++算法吗?k-means++和k-means的唯一区别就是起始点的选取过程, 你需要做的是将下面的代码: 1. self.centroids = [[self.data[i][r] for i in range(1, len(self.data))] 2. for r in random.sample(range(len(self.data[0])), 3. self.k)] 替换为: 1. self.selectInitialCentroids() 你的任务就是编写这个函数! 本文档使用 书栈(BookStack.CN) 构建 - 289 -

290.k-means聚类算法 解答 1. def distanceToClosestCentroid(self, point, centroidList): 2. result = self.eDistance(point, centroidList[0]) 3. for centroid in centroidList[1:]: 4. distance = self.eDistance(point, centroid) 5. if distance < result: 6. result = distance 7. return result 8. 9. def selectInitialCentroids(self): 10. """实现k-means++算法中的起始点选取过程""" 11. centroids = [] 12. total = 0 13. # 首先随机选取一个点 14. current = random.choice(range(len(self.data[0]))) 15. centroids.append(current) 16. # 开始选取剩余的点 17. for i in range(0, self.k - 1): 18. # 计算每个点到最近的中心点的距离 19. weights = [self.distanceToClosestCentroid(x, centroids) 20. for x in range(len(self.data[0]))] 21. total = sum(weights) 22. # 转换为权重 23. weights = [x / total for x in weights] 24. # 开始随机选取 25. num = random.random() 26. total = 0 本文档使用 书栈(BookStack.CN) 构建 - 290 -

291.k-means聚类算法 27. x = -1 28. # 模拟轮盘游戏 29. while total < num: 30. x += 1 31. total += weights[x] 32. entroids.append(x) 33. self.centroids = [[self.data[i][r] for i in range(1, len(self.data))] 34. for r in centroids] 本文档使用 书栈(BookStack.CN) 构建 - 291 -

292.安然事件 安然事件 安然事件 安然事件 你应该还对这件事有些印象吧?安然公司曾是一家超大型企业,有着千亿元的收入和两万名员工(微 软只有220亿收入)。 由于管理体制的破败和受贿,包括人为制造能源危机致使加州大停电,安然公司最终面临破产,大批 人员被判入狱。有一部名为“The Smartest Guys in the Room”的纪录片,读者可以到Netflix 或亚马逊上观看。 安然事件的确挺有趣的,不过这和数据挖掘有什么关系呢? 在调查过程中,美国联邦能源管理委员会收获了60万封公司内部邮件。这些邮件可以从网络上下载: http://en.wikipedia.org/wiki/Enron_Corpus https://www.cs.cmu.edu/~./enron/ 我们来用其中的一小部分数据来举例,下表整理了一些公司人员互通邮件的次数: 本文档使用 书栈(BookStack.CN) 构建 - 292 -

293.安然事件 可以在这里下载缩减后的数据集。完整的数据在这里,超过300MB。 动手实践 你能使用层次聚类算法将这些人分成若干类别吗? 解答 我们会通过两个人收发邮件的对象来计算相似度。比如我经常和Ann、Ben、Clara等人通信,你也一 样,那么我俩就是相似的: 但如果将你我之间的通信也计算进去: 可以看到,你向我发送了190次邮件,而我只向自己发送了2封邮件。用欧几里得距离来计算的话,在 不包含me和you这两列时,我们的距离是46,包含后距离是269!因此在计算两人的距离时需要排除 这个因素: 1. def distance(self, i, j): 2. # 针对安然数据进行的修正 3. sumSquares = 0 4. for i in range(1, self.cols): 本文档使用 书栈(BookStack.CN) 构建 - 293 -

294.安然事件 5. if k != i and k != j: 6. sumSquares += (self.data[k][i] - self.data[k][j]) ** 2 7. return math.sqrt(sumSquares) 得到的层次聚类结果是: 我还用k-means++算法进行了聚类,结果是: 这些结果很有趣,比如分类5中大都是贸易人员,分类7中则多是管理层。 本文档使用 书栈(BookStack.CN) 构建 - 294 -

295.安然事件 安然数据中还能挖掘出很多有趣的模式,去下载完整的数据集进行尝试吧! 你也可以对其它数据集进行聚类,看看是否有新的发现。 最后,恭喜完成第八章的学习! 本文档使用 书栈(BookStack.CN) 构建 - 295 -