Introduction

在这篇文章中,我将使用朴素贝叶斯分类器(Naive Bayes Classifier)打造一个简单的个人推荐系统:给定一篇中文文章(article),返回我喜欢(like)这篇文章的概率。用数学的语言来描述的话,我们要计算的是

$$P(like|aritcle)$$


根据贝叶斯公式,

$$P(like|aritcle) = \frac{P(article|like)P(like)}{P(article|like)P(like)+P(article|dislike)P(dislike)}$$

这样我们的问题就转化成计算\(P(like), P(dislike), P(article|like)\)\(P(article|dislike)\)了。当然,在计算\(P(article|like)\)或者\(P(article|dislike)\)的时候,我们要将文章(article)量化成可以计算的特征。假设给定一篇文章,我们可以提取特征:

$$x = (x_1, \dots, x_n)$$


那么,

$$P(article|like) = P(x|like) = P(x_1, \dots, x_n|like)$$


朴素贝叶斯分类器假设在确定喜欢或者不喜欢一篇文章的前提下,文章的特征\(x_1, \dots x_n\)是互相独立的。

在这个很强的假设下,

$$P(x_1, \dots, x_n|like) = P(x_1|like)P(x_2|like) \cdots P(x_n|like)$$

本文将使用Solidot的文章作为例子。

Features

我们首先要解决的问题是如何确定一篇文章的特征。让我们以Solidot的这篇文章为例:

硬件: 显卡价格开始下降了

过去一年的挖矿热导致显卡短缺,价格居高不下。但随着数字货币市场热度下降(或叫矿难),显卡开始不短缺了,价格也降下来了。今年 1 月,去百思买购买显卡的话,你会发现货架上几乎找不到几张,即使有价格也高得惊人, 8GB 的 Radeon 580 显卡售价 529.99 美元,店员还会告诉你错过了就买不到了;今天相同的显卡只需 419.99 美元,供应充足。除了 AMD 显卡价格下跌外,Nvidia 的显卡也存在类似现象。一名以太币矿工称,以太币的价格下跌导致了利润大幅减少,显卡短缺结束了。

最简单的想法是将文章的每个字作为一个特征,\(x_i\)表示某个字是否出现在文章中。本来在朴素贝叶斯中我们就假设特征之间没有关系,如果现在还将文章每个字单独拿出来作为特征就有点太过扭曲原文的意思啦。比如“过去”和“去过”这两个意思完全不一样的词语都含有“过”和“去”两个字,使用每个字作为特征的模型就无法区分这两个词语了。

为了使我们的模型不至于太过简单,我使用了文章中出现的词语作为特征。并且,我还将只有一个字的词语、标点符号和数字等去掉。

首先我们使用结巴分词对文章进行精确模式分词:

import jieba
text = "过去一年...短缺结束了。"
words = list(jieba.cut(text, cut_all=False))
print("/".join(words))
过去/一年/的/挖矿/热/导致/显卡/短缺/,/价格/居高不下/。/但/随着/数字/货币/市场/热度/下降/(/或/叫/矿难/)/,/显卡/开始/不/短缺/了/,/价格/也/降下来/了/。/今年/ /1/ /月/,/去/百思买/购买/显卡/的话/,/你/会/发现/货架/上/几乎/找/不到/几张/,/即使/有/价格/也/高得/惊人/,/ /8GB/ /的/ /Radeon/ /580/ /显卡/售价/ /529.99/ /美元/,/店员/还会/告诉/你/错过/了/就/买不到/了/;/今天/相同/的/显卡/只/需/ /419.99/ /美元/,/供应/充足/。/除了/ /AMD/ /显卡/价格/下跌/外/,/Nvidia/ /的/显卡/也/存在/类似/现象/。/一名/以太/币/矿工/称/,/以太/币/的/价格/下跌/导致/了/利润/大幅/减少/,/显卡/短缺/结束/了/。

然后我们提取长度大于1的非数字词语。

def check(word):
    word = word.strip()
    try:
        float(word)
        return False
    except Exception:
        pass
    return len(word) > 1
words = set(filter(check, words))
print(words)
{'类似', '百思买', '大幅', '下降', '导致', 'AMD', '今年', '利润', '美元', '显卡', '售价', '供应', '开始', '今天', '居高不下', '一年', '发现', '市场', '一名', '除了', '随着', '几乎', '短缺', '货币', '的话', '还会', '存在', '错过', '结束', '高得', '8GB', '店员', '以太', '过去', '购买', '矿工', '充足', '下跌', '矿难', 'Radeon', '不到', '相同', '价格', '减少', '即使', '降下来', 'Nvidia', '数字', '惊人', '告诉', '货架', '买不到', '热度', '几张', '挖矿', '现象'}

上面这个这个集合中的词语就是文章的特征(features)了。

Model and Data

朴素贝叶斯分类器我在之前的一篇笔记Generative Model就讲过了。我们的模型需要训练以下参数:\(P(like), P(dislike), P(word|like)\)\(P(word|dislike)\)。其中word要遍历所有在数据中出现的词语。如果我们已经有数据了,这些参数都很容易计算出来,因为这些参数都是能通过统计得出来。比如\(P(word|like)\),我们只要统计喜欢的文章总数\(n\),以及在喜欢文章中包含word这个词的文章数量\(w\),那么

$$P(word|like) = \frac{w}{n}$$


其他参数都如此类似通过简单统计得到,这里就不一一赘述了。详情请看我的笔记Generative Model

Solidot将文章分成了很多个子类别,比如“Linux”,“科学”,“科技”等等。我简单地将“科学”、“科技”、“移动”和“安全”这四个子类别下的文章标记成我喜欢,而将“苹果”、“硬件”、“软件”和“游戏”下的文章标记成我不喜欢的。然后我利用爬虫将Solidot上这8个类别从2013到2017年的文章全部抓取下来。

在Raspberry Pi3上使用10个线程耗时1个半小时,我一共抓取了11373篇文章(有重复,某些文章会在多个类别出现),其中标记成喜欢的文章有8373篇,标记成不喜欢的有3000篇。经过6分钟的训练,我一共得到62167个不重复的词语。

使用这个模型和训练数据集,我计算出我喜欢看上述样例文章“硬件: 显卡价格开始下降了”的概率:1.9263403800846358e-11。这个结果跟我的训练集相符合:我将“硬件”类别的文章标记成不喜欢了。此时此刻,我对Solidot首页文章的喜欢概率如下。

Mon 30 Apr 21:49:35 EDT 2018

中国正在大规模测试大脑情绪监测技术: 1.0000
硬件:显卡价格开始下降了: 0.0000
男子起诉法国扣押了他的 France.com 域名: 0.0000
游戏:中国逮捕 15 名《绝地求生》外挂开发者: 0.0000
盖茨称改善美国教育比降低婴儿死亡率还难: 1.0000
金正恩否认停止核试验是因为测试地点塌陷: 1.0000
俄罗斯为封杀 Telegram 屏蔽了 1800 万 IP,包括本国网站的 IP: 0.9999
安全:如何恢复被删除的微信聊天记录: 1.0000
为什么日本要抓捕汉化组: 0.0000
idle:研究称久坐可能让人变蠢: 1.0000
审查让调查性报道逐渐消失: 0.8247
科学:受精卵如何创造出整个身体: 1.0000
科学:银河系可能有许多流浪的超大质量黑洞: 1.0000
安全:朝鲜黑客在和平声中四处出击: 1.0000
安全:安全专家公布能崩溃所有 Windows 的 PoC 代码: 0.7551
多个国家利用加拿大公司的设备审查内容: 1.0000
Linux:GIMP 2.10.0 释出: 0.0000
三星镁光海力士被控合谋操纵 DRAM 价格: 0.0000
知乎更新隐私条款,用户没法“不同意”: 0.0058
Windows 10 April 2018 Update 新特性: 0.0000

Discussions

其实我对Solidot上面的文章都挺感兴趣的,上面只是为了简单获得数据而将一些文章标记成喜欢,另一些标记成不喜欢。所以以上计算出的概率并不能真实反映我的喜好。从另一个角度去思考,上面计算出来的概率其实是属于子类别“科学”、“科技”、“移动”或者“安全”的概率。