惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
V
V2EX
爱范儿
爱范儿
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
罗磊的独立博客
小众软件
小众软件
Vercel News
Vercel News
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
V
Visual Studio Blog
J
Java Code Geeks
P
Proofpoint News Feed
MongoDB | Blog
MongoDB | Blog
B
Blog
美团技术团队
量子位

Axton - 无垠

使用通行密钥 PRF 扩展在浏览器中安全加密 - 无垠 MDx Blog 现已随 MDx 1.9.5 发布 - 无垠 谈谈 WebAuthn - 无垠 不止更亮:在 Web 中使用 HDR - 无垠 无垠 2022 年度总结 - 无垠 阿尔卑斯的夏与冬:瑞士双游记 - 无垠 无垠 2021 年度总结 - 无垠 前端时间国际化 101 - 无垠 无垠 2020 年度总结 - 无垠 MDx 中增强页面可访问性的细节 - 无垠 GitHub 2019 统计无垠版 - 无垠 Milkice 跨年红包解谜 Writeup 及无垠跨年红包解谜答案 - 无垠 无垠 2019 年度总结 - 无垠 Razer Blade 灵刃 15 2019 精英版简评 - 无垠
NLP 找门:用朴素贝叶斯进行文本分类 - 无垠
Axton · 2020-01-29 · via Axton - 无垠

有一天,神收到了一封垃圾邮件,于是世界上就有了朴素贝叶斯算法。

为什么这篇文章的标题是 NLP 找门呢?因为如果你看完了这篇文章,你就可以准备入门自然语言处理 (Natural Language Processing) 了。我会把这篇文章当作一篇彻彻底底的 NLP 入门文章来写,尽量避免“专业高端”词汇和“全是奇怪符号”的式子,这样只要你有高中数学基础,就能毫无压力地看完这篇文章,学会使用朴素贝叶斯分类算法分类文本。

让我们开始吧。

所以,啥是朴素贝叶斯?

简单来说,朴素贝叶斯 (Naïve Bayes) 是一个简单但高效的分类算法,在进行不复杂的文本分类时高效且拥有不低的准确度,判断垃圾邮件就是用朴素贝叶斯进行文本分类的一个经典例子。当然朴素贝叶斯分类不仅能用在 NLP 中,在许多分类问题中朴素贝叶斯也有非常好的效果,但我们今天只关注它在 NLP 中的应用。

要了解朴素贝叶斯,我们要先来了解一下贝叶斯定理。

每增加一个数学公式都会使读者减半。

那么,就让我们先来看看贝叶斯定理的公式吧(笑

$$P(A|B) = \frac{P(AB)}{P(B)} = \frac{P(B|A)P(A)}{P(B)}$$

其实还是很简单的,运用高中的条件概率知识就能理解。本质上,贝叶斯定理给出了一种方法,使得我们可以在 \(P(A|B)\) 和 \(P(B|A)\) 之间互相转换,因为通常情况下它们是不一样的。

要更好地理解,请考虑这样一个例子:

假设有一个学校图书馆,图书管理员正为找不到某本书而发愁。已知老师有 70% 的意愿借走这本书,而是学生的意愿是 30%,这个学校的师生比例是 1:10,那么借走这本书的人是老师的概率有多大?

设学校总人数为 \(T\),我们可以很容易地写出这样的一个式子:

$$\begin{align} P & = \frac{T\cdot\frac{1}{11}\cdot 70\%}{T\cdot\frac{1}{11}\cdot 70\%+T\cdot\frac{10}{11}\cdot 30\%} \\ & = \frac{70\%}{70\%+10\times30\%} \\ & = \frac{7}{37} \end{align}$$

这就是贝叶斯定理了!等等,你可能会挠挠头,这哪里是贝叶斯了?别急,如果我们把最上面的式子换个字母的话...

$$P(H|E) = \frac{P(E|H)P(H)}{P(E)}$$

在这里,\(H\) 指 Hypothesis,即假设,而 \(E\) 指 Evidence,即证据。这样,这个式子就很好理解了,在我们上面的例子里,借走书是证据,那么 \(P(E)\) 就是某个人选择借走这本书的概率;这个人是老师是假设,那么这个人是老师的概率是 \(P(H)\)。注意这里的概率指在学校中抽一个人是老师的概率,并不是“在借走书的前提下”这个人是老师的概率,而“在借走书的前提下”这个人是老师的概率应该是 \(P(H|E)\),也正是我们要求的概率。同样地,而“这个人是老师”的前提下借走书的概率就是 \(P(E|H)\) 了。

那么再来看看上面我们凭小学知识就列出的计算式,如果把里面的具体数据换成概率来表示的话,这个式子就会变成...

$$P(H|E) = \frac{T\cdot P(E|H)P(H)}{T\cdot P(E|H)P(H) + T\cdot P(E|\neg H)P(\neg H)}$$

好吧,这里解释一下,\(\neg\) 符号表示“非”,所以 \(P(E|\neg H)\) 表示在“这个人不是老师”的前提下借走书的概率。而 \(T\cdot P(E|H)P(H)\) + \(T\cdot P(E|\neg H)P(\neg H)\),即“可能借走书的老师的数量 + 可能借走书的学生的数量”,就是“可能借走书的人的数量”了,也就是 \(T\cdot P(E)\)。上下消去 \(T\),我们就能得到上面的式子了。

$$P(H|E) = \frac{P(E|H)P(H)}{P(E)}$$

这就是贝叶斯了定理。如果你还是不太清楚,可以去看看 3Blue1Brown 的这个视频,图形化的讲解会清晰很多。

这里我要提一下这个式子里各部分的专有名称了(“专业高端”词汇警告),你可以不记住,直接看后面。

  • \(P(H|E)\) 叫做 \(H\) 的后验概率,反之亦然
  • \(P(H)\) 叫做 \(H\) 的先验概率,反之亦然
  • 特别地,我们把 \(P(E|H)\) 称作“似然值”,即 likelihood

那什么是朴素贝叶斯呢?按上面所说的,朴素贝叶斯是一种分类算法。简单来说,朴素贝叶斯将一个对象的各个特征考虑为互相独立,然后根据这些特征的概率的乘积来判断对象所属的分类。基本原理如下:

$$P(H|E) = \frac{P(E|H)P(H)}{P(E)} \propto P(E|H)P(H) = P(H)\prod_{i}P(W_i|H)$$

在这里,\(W_i\) 指某一对象的第 \(i\) 个特征,对于文本分类来说,这就是一段文本中的某个单词。

朴素贝叶斯之所以“朴素”,是因为它要求各个特征间是独立的,在文本分类中也就是各个单词之间互不干扰。虽然思路简单的代价是适用范围变窄,不过由于这样的简化在很多情况下已经足够了,因此实际上朴素贝叶斯的应用范围非常广。你看朴素贝叶斯 Naïve Bayes 的缩写都是 NB 了,能不厉害吗(逃

那么,咋分类啊

看来你这下完全听懂了呢(笑),是时候看看如何在 NLP 中应用朴素贝叶斯了。和上面一样,我们用一个具体的例子来说明。我们的目标是让电脑学会分类美国共和党和民主党的演讲稿,由于两个党派的演讲风格不同,所以这样的分类在理论上是可行的。

上面我们要用到的数据集,你可以下载下来一起动手玩一玩。先说一下数据集的数据结构吧,压缩包里有两个文件,train.txt 将会被当作训练数据集,而 test.txt 则会作为训练结束后的验证数据集。两个文本文件里数据的结构是类似的,就像这样:

BLUE WELL I AM SO HONORED AND PERSONALLY UNKNOWNWORD TO BE HERE...
RED THANK YOU . THIS IS QUITE AN INSTITUTION . IT'S GOOD TO BE...

每行都是一篇演讲稿,每行的第一个单词指明了这篇演讲稿所属的党派,RED 指共和党,而 BLUE 指民主党。所有单词和符号都已经被转为大写并由空格分隔方便处理。train.txt 有共和党演讲稿和民主党演讲稿各 23 篇,test.txt 有 6 篇共和党演讲稿,12 篇民主党演讲稿。

明白了?那我们就开始吧。

捋捋思路

首先,我们需要考虑如何在文本分类中应用朴素贝叶斯。很简单,按朴素贝叶斯的思路,计算每个词在某一分类下的出现概率,然后将某篇文章的所有词的概率相乘,再乘以该分类的先验概率,就可以得到某篇文章在某一分类下的概率。

$$P(Class|Article) = P(Class)\prod_{i}P(Word_i|Class)$$

各个分类概率都计算完成后,概率最高的那个分类就是这篇文章可能所属的分类。这个思路的核心就是用词决定了文本风格,文本的不同类别用词会有差异,只要能量化这些差异就可以分类文本。在我们的例子中,我们可以从 train.txt 中统计各个词汇的出现情况,然后用 test.txt 中的数据按上面的算法验证我们算法的准确性。

这个思路很简单也很清晰,但还有一些问题需要解决。第一,有的时候,我们的测试数据集中可能会出现一个在训练数据集中没有出现过的词语。这个时候,朴素贝叶斯的计算结果会是 0。如果我们把 0 乘进式子中,那就别想得到正常的结果了。所以,我们还需要对计算某一单词在某一分类中的式子稍加改进。使用拉普拉斯平滑,就可以避免出现概率为 0 的情况。别被名字吓到,拉普拉斯平滑是一种非常简单的平滑方法:在分子上 +1,在分母上加整个取值范围,这样就可以给整个分式加上非常微小的偏移,避免出现 0。

$$\begin{align} P(Word_i|Class) & = \frac{Word_iCountInClass}{AllWordCountInClass} \\ & \approx \frac{Word_iCountInClass + 1}{AllWordCountInClass + UniqueWordCount} \end{align}$$

第二,对于长文本,大部分词语在某一分类中的出现概率是远小于 1 的,加上长文本词汇量大,往往概率相乘的结果会非常小。受限于计算机处理浮点数的原理,精确处理这么小的数字是很麻烦的。幸好,运用一些简单的数学知识就可以将其转化为更精确的表达,那就是取对数。

首先,将概率计算结果取对数并不影响我们的计算结果。因为取对数是一个单调递增的操作,而我们计算概率只是为了排序选择概率最高的分类,因此取对数是不影响我们排序的。而把多项式取对数,等于把多项式的每一项取对数后相加。所以我们有:

$$\lg{\Big(P(Class)\prod_{i}P(Word_i|Class)\Big)} = \lg{P(Class)} + \sum_i\lg{P(Word_i|Class)}$$

大部分情况下,在每一次取对数的时候,要取对数的数字的大小,即 \(P(Word_i|Class)\) 尚还在计算机能处理的范围内,因此我们可以放心地使用取对数的方法,避免计算机精度不够影响结果。

第三就是在某些情况下,可能会有部分词语干扰计算结果,如 and, is 这类的被大量使用的中性词。如果希望得到更好的结果,我们可以维护一个停用词表,在计算时排除停用词即可。或者,我们可以在计算完每个单词的出现数量后,排除数量最多的前 \(N\) 个单词,避免这些单词过多地影响计算。

写点代码

现在我们终于可以开始实战,写点代码了。我会用简单的 Python 来表达思路。好了,理一理思绪,第一步我们要做的,是统计训练数据集中的用词情况。具体来说,根据上面的思考,我们需要统计的有:

  • 每个单词在各分类中出现的数量
  • 各分类中的不重复词数量

用 Python 简单表示如下:

# 读入数据过程略...
data_raw = "读入的数据".split('\n') # 按行分隔
data_blue = []
data_red = []
word_count_blue = {}
word_count_red = {}

for line_data in data_raw:
    word_list = line_data.split(" ") # 分隔单词
    if word_list[0] == "BLUE":
        for i in range(1,len(word_list)):
            if not is_excluded(word_list[i]): # 判断是否为停用词
                data_blue.append(word_list[i])
                # 统计单词出现次数
                word_count_blue.setdefault(word_list[i], 0)
                word_count_blue[word_list[i]] += 1
    elif word_list[0] == "RED":
        # Class = red 时同理...

#统计非重复词
unique_words_blue = list(set(data_blue))
unique_words_red = list(set(data_red))

训练过程到这里就结束了,计算机已经知道了各政党演讲的用词习惯。很简单吧?接下来我们就要使用测试数据集来测试准确度了。这里开始就涉及到朴素贝叶斯的计算了,可能会稍微复杂一点点。

import math
#读入测试数据过程略...
test_data_raw = "读入的数据".split('\n') # 按行分隔
test_data = []

for line_data in test_data_raw:
    if line_data[0] == "BLUE":
        content_data = line_data.split(" ").pop(0)
        test_data.append({"class": "blue", "content": content_data})
    elif line_data[0] == 'RED':
        # ...

for line_data in test_data:
    posibility_blue = 0
    for word in line_data["content"]:
        # 计算各单词概率,取对数后相加,使用了拉普拉斯平滑
        if word in word_count_blue:
            posibility_blue += math.log((word_count_blue[word]+1)/(len(data_blue)+len(unique_words_blue)+len(unique_words_red)))
        else:
            posibility_blue += math.log(1/(len(data_blue)+len(unique_words_blue)+len(unique_words_red)))
    # 最后加上该分类概率的对数
    posibility_blue += math.log(len(data_blue)/(len(data_blue)+len(data_red)))

    # 计算 Red 同理...

计算完成后,我们就得到每篇演讲稿在各分类下的概率了。之后,简单的比较就能得出推断的分类:如果 \(P(Red|Article)\) 大于 \(P(Blue|Article)\),那么这篇演讲稿就更可能是共和党的演讲稿,反之则是民主党的演讲稿。

很好理解吧?那么这么简单的思路,准确性怎么样呢?对于我们的例子,这是我设置了个别停用词后的结果:

+-----+-------+--------------+--------------+-------+-------+
|   ID|  Class|        P(Red)|       P(Blue)|  Guess| Status|
+-----+-------+--------------+--------------+-------+-------+
|    1|   Blue|  -23204.68377|  -22998.67066|   Blue|      √|
|    2|   Blue|  -16438.44625|  -16137.48257|   Blue|      √|
|    3|   Blue|  -33468.81214|  -32567.61801|   Blue|      √|
|    4|   Blue|    -8606.2193|   -8601.50426|   Blue|      √|
|    5|   Blue|  -12430.97436|  -11935.70662|   Blue|      √|
|    6|   Blue|  -44033.02883|  -43877.55367|   Blue|      √|
|    7|   Blue|   -16947.2851|  -16758.57542|   Blue|      √|
|    8|   Blue|  -26957.26997|  -26889.62444|   Blue|      √|
|    9|   Blue|  -27503.73985|  -27249.21828|   Blue|      √|
|   10|   Blue|   -20528.4457|   -19991.1248|   Blue|      √|
|   11|   Blue|  -20337.96493|  -19860.12831|   Blue|      √|
|   12|   Blue|  -28409.28489|  -28118.98017|   Blue|      √|
|   13|    Red|  -13756.01015|  -14488.11751|    Red|      √|
|   14|    Red|  -17221.22732|  -17710.15936|    Red|      √|
|   15|    Red|  -17397.45136|  -17899.98659|    Red|      √|
|   16|    Red|  -10724.69095|  -11092.77837|    Red|      √|
|   17|    Red|  -10402.40027|  -10859.48681|    Red|      √|
|   18|    Red|   -9371.53792|    -9669.6769|    Red|      √|
+-----+-------+--------------+--------------+-------+-------+
|                                      Total: 18/18, 100.0% |
+-----------------------------------------------------------+

100% 的准确率哦!事实上,对于我们的这个例子,就算不设置停用词,我们仍能达到 100% 的分类准确率。朴素贝叶斯分类的确很 NB 呢。

更进一步

我们的探索到这里就结束了,但如果你有兴趣,完全可以继续探索下去。我们的例子是一个非常简化的例子,在实际情况中,还有很多问题需要解决。比如,对于中文及类似语言,不存在拉丁语系的天然分词结构,而朴素贝叶斯的文本分类是基于单词的,那么中文的分词就会是个问题;再比如,对于朴素贝叶斯分类来说,词语之间的顺序是不影响分类结果的,但这就会导致“今天心情很好,但昨天不好”和“昨天心情很好,但今天不好”在朴素贝叶斯看来是一样的,要想获得更好的结果,我们必须考虑词语的顺序。

这些问题,今天我们就不再深究了,但你可以自己探索。比如,引入思路同样很简洁的马尔科夫链,我们就可以让计算机学会考虑词语间的顺序,不过那就会是另一个话题了。