贝叶斯文本分类算法的JavaScript实现

一. 前言

img

img

( < 黑客与画家 >, Paul )

JavaScript(浏览器端)分词 | Lian (kyouichirou.github.io)这篇文章中提及js分词的实现. 本文则是js版本贝叶斯分类算法的实现, Kyouichirou/BiliBili_Optimizer: enjoy and control bilibili (github.com).

使用统计方法来实现垃圾内容过滤, 很早就想写的了, 但是由于在js端没有好的分词方案(), 一直没捣鼓, 直到发现原来js原生就支持分词, 这个想法马上付诸实现.

以下是对贝叶斯和文本分类算法的简单回顾, 逐步从基础推导出整个数学公式, 并根据python sklearnnative_bayes进行代码的修正, 给出了普通方式计算的代码.

由于需要在毫秒内实现, js脚本使用的分词方案是js原生分词api.

1.1 贝叶斯

关于贝叶斯和传统频率之间的一些差异.

  • 频率学派 - Frequentist - Maximum Likelihood Estimation (MLE, 最大似然估计)
  • 贝叶斯学派 - Bayesian - Maximum A Posteriori (MAP, 最大后验估计)
FrequentistBayesian
频率论方法通过大量独立实验将概率解释为统计均值( 大数定律) 贝叶斯方法则将概率解释为信念度( degree of belief) ( 不需要大量的实验)
频率学派把未知参数看作普通变量( 固定值) , 把样本看作随机变量贝叶斯学派把一切变量看作随机变量
频率论仅仅利用抽样数据贝叶斯论善于利用过去的知识和抽样数据

贝叶斯学派与频率学派有何不同? - 知乎

统计学里频率学派(Frequentist)与贝叶斯(Bayesian)学派的区别和在机器学习中的应用 - 知乎

先简单回顾一下贝叶斯.

P(AB)=P(A)P(BA)P(B)P(A),P(BA),P(AB),,:P(AiB)=P(BAi)P(Ai)jP(BAj)P(Aj)P(A|B) = \frac{P(A)P(B|A)}{P(B)}\\ P(A), 先验\\ P(B|A), 似然\\ P(A|B), 后验\\ 延申扩展, 全概率公式: P(A_i|B) = \frac{P(B|A_i)P(A_i)}{\sum_j{P(B|A_j)P(A_j)}}

符号 含义
B 已有的数据(data)
A 要估计的参数(parameter)
p(A) 先验概率(prior)
p(A|B) 后验概率(posterior)
p(B) 数据分布(evidence)
p(B|A) 似然函数(likelihood of A w.r.t. B)

一个简单的例子, 假设抛硬币, 前99次均是正面, 请问再抛1次, 出现正面的概率?

常见的答案可能是:

答案1: 频率学派, 50%(当然这是建立在假设硬币质地均匀的假设), 因为出现正反面是相互独立的事件, 前99次的抛投结果不影响下一次的抛投, 所以正反均为50%.

显然答案1给出的结果不是很符合观测到的现实情况, 因为实际的测试很大概率表明因该是正面.

但是需要注意并不是说频率学派给出的答案有问题, 需要注意假设的前提: 硬币均匀.

由于:

P()=12990,0P(正面) = \frac{1}{2 ^ {99}} \sim 0, 连续出现正面的概率趋近于0

理论上连续出现99次正面的概率近乎于0, 称之为小概率事件, 侧面课推断这个硬币是存在问题的(如重量不均匀)亦或者抛投方式存在问题, 这和硬币均匀假设相违背.

答案2: 贝叶斯(后验分布)

用一句话来说, beta分布可以看作一个概率的概率密度分布, 当你不知道一个东西的具体概率是多少时, 它可以给出了所有概率出现的可能性

P,Beta.n,k,P,:α:k+1;β:nk+1:E=α(α+β)=k+1n+2k=n=99=99+199+21假设出现正面概率P, 符合Beta分布.\\ 进行n次实验, k次成功, P的条件概率, 参数: \alpha: k + 1; \beta: n - k + 1\\ 则期望为: E = \frac{\alpha}{(\alpha + \beta)} = \frac{k + 1}{n + 2} \\ k = n = 99\\ = \frac{99 + 1}{99 + 2} \sim 1

贝塔分布( Beta Distribution) 简介及其应用 | 数据学习者官方网站(Datalearner)

1.1.1 抓酒鬼问题

问题: 已知某酒鬼有90%的概率会出去喝酒, 喝酒只去固定三家酒吧(假设为A, B, C, 均匀分布), 还有10%的概率呆在家里. 今天警察找了其中两家酒吧都没有找到酒鬼. 问: 酒鬼在第三家酒吧的几率?

P(A)+P(B)+P(C)=90%:P(A)=P(B)=P(C)=30%P(home)=10%,100,(30),(30),绿(30),(10)绿,:P(^)=30/40=0.75P(A) + P(B) + P(C) = 90\%\\ 即: P(A) = P(B) = P(C) = 30\%\\ P(home) = 10\%\\ 将上述问题转化为抽小球问题, 假设有100个球, 分别为红(30), 黄(30), 绿(30), 白(10)\\ 警察找了其中两家的行为就相当于去掉红黄绿中的任意的两组, 问剩下的球中抽到不是白色的球的概率:\\ P(\hat{白}) = 30 / 40 = 0.75

使用贝叶斯的方法来计算:

:C,;C^,D,;D^,P(D)=0.9,P(CD)=2/3;,P(C^D^)=1;,:P(C^D),(,2,)P(DC^)=P(D)P(C^D)P(C^)=P(D)P(C^D)P(D)P(C^D)+P(D^)P(C^D^)=0.91/30.91/3+0.11=0.30.4=0.75P(C^),,:P(D)P(C^D),;P(D^)P(C^D^),P(C^)=P(D)P(C^D)+P(D^)P(C^D^)存在以下事件:\\ 事件C, 被抓; 事件\hat{C}, 没被抓\\ 事件D, 喝酒; 事件\hat{D}, 没喝酒\\ P(D) = 0.9, 喝酒\\ P(C|D) = 2/3; 出去喝酒, 被抓\\ P(\hat{C}|\hat{D}) = 1; 呆在家里, 没被抓\\ 目标: P(\hat{C}|D), 没被抓(前提, 警察查了2个酒吧, 但是没找到)的情况下在喝酒的概率\\ P(D|\hat{C}) = \frac{P(D)P(\hat{C}|D)}{P(\hat{C})}= \frac{P(D)P(\hat{C}|D)}{P(D)P(\hat{C}|D) + P(\hat{D})P(\hat{C}|\hat{D}) } = \frac{0.9 * 1 / 3}{0.9 * 1/3 + 0.1 * 1} = \frac{0.3}{0.4} = 0.75\\ \\ P(\hat{C}) , 根据全概率公式, 这个很容易得出:\\ P(D)P(\hat{C}|D),\quad 喝酒时不被抓; \\ P(\hat{D})P(\hat{C}|\hat{D}),\quad 呆在家时不被抓\\ P(\hat{C}) = P(D)P(\hat{C}|D) + P(\hat{D})P(\hat{C}|\hat{D})


以下使用AI对问题进行求解:

将问题文字调整一下(酒鬼/警察可能触发chatgpt违禁词汇):

已知小明有90%的概率都会出去喝酒, 喝酒只去固定三家酒吧, 还有10%的概率呆在家里. 今天我找了其中两家酒吧都没有找到小明. 问: 小明在第三家酒吧的几率? 请使用贝叶斯公式进行求解

文心一言:

(文心一言的响应速度非常慢, 相比之下ChatGPT几乎同步生成结果)

尽管指定使用贝叶斯进行求解的情况下, 文心一言还是给出错误的答案.

已知某酒鬼有90%的日子都会出去喝酒, 喝酒时他会等概率地去三家酒吧A, B, C.
还有10%的概率, 他会呆在家里. 今天警察找了其中两家酒吧都没有找到酒鬼.
我们需要计算酒鬼在第三家酒吧的几率.

假设酒鬼去酒吧A, B, C的概率都是相等的, 即 P(A) = P(B) = P(C) = 1/3.
酒鬼呆在家里的概率是 P(Home) = 0.1.

根据题目, 警察已经找了酒吧A和酒吧B都没有找到酒鬼.
因此, 酒鬼在酒吧A和酒吧B的概率都是0, 即 P(A) = P(B) = 0.

由于酒鬼只能去酒吧A, B, C或者呆在家里, 这些事件是互斥的, 所以他们的概率之和为1.
因此, 酒鬼在酒吧C的概率 P(C) 可以通过以下公式计算:
P(C) = 1 - P(A) - P(B) - P(Home)

现在我们要来计算这个概率.
计算结果为: P(C) = 0.9
所以, 酒鬼在第三家酒吧的几率是 90%.


在这个问题中, 我们要使用贝叶斯公式来更新我们对小明在第三家酒吧的概率的估计.
首先, 我们需要定义一些概率:
P(D) = 小明出去喝酒的概率 = 0.9
P(N) = 小明呆在家里的概率 = 0.1
P(C1|D) = 小明如果出去喝酒, 去第一家酒吧的概率 = 1/3
P(C2|D) = 小明如果出去喝酒, 去第二家酒吧的概率 = 1/3
P(C3|D) = 小明如果出去喝酒, 去第三家酒吧的概率 = 1/3
P(C1|N) = 小明如果呆在家里, 去第一家酒吧的概率 = 0
P(C2|N) = 小明如果呆在家里, 去第二家酒吧的概率 = 0

我们的目标是计算 P(C3|E), 即在已知小明不在第一家和第二家酒吧的情况下, 他在第三家酒吧的概率.
这里, E 表示事件" 小明不在第一家和第二家酒吧" .

根据贝叶斯公式, 我们有:
P(C3|E) = [P(C3|D) × P(D) × P(E|C3,D)] / P(E)

其中, P(E|C3,D) 是在小明去第三家酒吧且出去喝酒的条件下, 他不在第一家和第二家酒吧的概率, 这个概率是1, 因为这三家酒吧是互斥的.
P(E) 是在已知小明不在第一家和第二家酒吧的情况下, 他不在任何一家酒吧的概率, 这个概率可以通过全概率公式计算:
P(E) = P(D) × [P(E|C1,D) + P(E|C2,D) + P(E|C3,D)] + P(N) × P(E|N)

现在, 我们可以进行计算.
计算结果为: P(C3|E) = 3/10
所以, 在已知小明不在第一家和第二家酒吧的情况下, 他在第三家酒吧的概率是 3/10.

ChatGPT

当指定使用贝叶斯进行求解时, 可以准确给出答案.

img

根据题目描述, 我们可以使用贝叶斯公式来求解小明在第三家酒吧的几率.

设事件A为小明在第三家酒吧, 事件B为今天我找了其中两家酒吧都没有找到小明.

根据题目信息:

  • P(A) = 1/3( 因为小明只会去三家酒吧中的一家)
  • P(B|A) = 1( 如果小明在第三家酒吧, 那么我找了其中两家酒吧都没有找到小明的概率为1)
  • P(B) = P(B|A)P(A) + P(B|Ac)P(Ac) = 1 * 1/3 + (1/3)^2 = 4/9

根据贝叶斯公式: P(A|B) = P(B|A)P(A) / P(B) = 1 * 1/3 / (4/9) = 3/4 ≈ 75%

所以, 根据贝叶斯公式, 小明在第三家酒吧的几率为75%.

1.2 sklearn

1.9. Naive Bayes - scikit-learn 1.4.1 documentation

sklearn native bayes模块中提供了以下5中不同的贝叶斯, 以下主要使用的多项式贝叶斯, 这种方法在文本分类问题上有着很好的表现.

img

二. 文本分类

文本分类, 暂时先不看上面的贝叶斯公式, 单凭直觉, 也能够大概知道其基础逻辑.

I was born in japan, I like japanese food, but I can say chinese => j
I am a chinese, and I like japan => c

I was born in us, but i am a japanese => ?

假设存在着这样已经被标签标记的内容.

由于被标签的内容, 其每个单词出现的概率是不一样的, 根据单词出现的概率的差异来判断其所属.

即: P(J) 和 P(chinese) & P(japanese) & P(born) & .....存在着某种关联, 通过判断这个关联程度(概率大小)来确定内容的分类所属.

最为常见的垃圾邮件分类, 例如识别诈骗邮件, 垃圾邮件很可能会出现'中奖', '银行卡', '转账', '点击'....等等词汇.

通过预先收集的垃圾邮件和正常邮件, 作为预判断的基础材料, 通过后期的手动标记加以修正, 不断修正模型以不断提高识别率.

from sklearn.naive_bayes import MultinomialNB
from sklearn.naive_bayes import MultinomialNB

contents = [
    'I was born in japan, I like japanese food, but I can say chinese',
    'I am a chinese, and I like japan'
]
y = [0, 1]
tf = CountVectorizer()
X = tf.fit_transform(contents)
X_test = tf.transform(['I was born in us, but i am a japanese'])
m = MultinomialNB()
m.fit(X, y)
# 预测
m.predict(X_test)

array([0])

其数学表示形式为:

P()=P()P()P():P()=P()P()P(),,广:Aii,B,AiP(BA1,A2,,An)=P(A1B)P(A2B)P(AnB)P(B)P(A1)P(A2)P(An)=P(B)Πi=1nP(AiB)Πi=1nP(Ai)P(Ai)=0,P(Ai)=wi+1n+kwi,;  n;  k,,2,,,ln(P(BA1,A2,,An))=ln(P(B))+ln(Πi=1nP(AiB))ln(Πi=1nP(Ai))=ln(P(B))+i=1nln(P(AiB))i=1nln(P(Ai))ln(X),X>0,P(Ai),P(AiB)>0P(类别|特征) = \frac{P(特征|类别)P(类别)}{P(特征)}\\ 例如: P('垃圾邮件'|'中奖') = \frac{P('中奖'|'垃圾邮件')P('垃圾邮件')}{P('中奖')},\\ 即在出现'中奖'这个词, 邮件被判定为垃圾邮件的概率\\\\ 推广到全概率公式:\\ 假设存在事件A_i为出现在词汇表的第i个词, B为垃圾邮件, 每个事件A_i相互独立\\ P(B|A_1,A_2,⋯,A_n)=\frac{P(A_1 |B)P(A_2 |B)⋯P(A_n |B)P(B)}{P(A_1)P(A_2)⋯P(A_n)}=\frac{P(B)Π_{i=1}^n P(A_i|B)}{Π_{i=1}^n P(A_i)}\\ 需要注意的是存在P(A_i)= 0的问题, 所以这个时候需要引入拉普拉斯平滑处理\\ P(A_i) = \frac{w_i + 1}{n + k}\quad w_i, 词汇数量;\;n为样本总数;\; k为类别数量, 二分类, 即为2\\ \\ \\ 由于在实际的计算中, 由于是概率的累乘, 会因为数据偏小的向下溢出, 需要取对数处理\\ \ln(P(B|A_1,A_2,⋯,A_n)) =\ln(P(B)) + \ln(Π_{i=1}^n P(A_i|B)) - ln(Π_{i=1}^n P(A_i))\\ = \ln(P(B)) + \sum_{i = 1}^n\ln(P(A_i|B)) - \sum_{i=1}^n\ln(P(A_i))\\ 因为\ln(X), X > 0, 即任意P(A_i), P(A_i|B)均需要 > 0

这里还涉及到一个问题, 即统计词汇的方式, 一般常用的模型: 多项式模型和伯努利模型.

img

这里采用的是多项式模型, 计算的是词汇出现的次数.

但需要注意的是, 在实际的计算中, 代码不会完全按部就班的来算, 这种计算方式很浪费资源.

中奖, 点击, 银行卡, 大奖   垃圾

公司, 银行卡, 财务, 报表   正常

财务 银行卡 公司 月报   ?

P(正常|财务,银行卡,公司, 月报) = ?

P(垃圾|财务,银行卡,公司, 月报) = ?

P(,,,)=P()P()P()P()P()P()P()P()P()P(,,,)=P()P()P()P()P()P()P()P()P(),P()=wwP()=DDln(P())+ln(P())+ln(P())+ln(P())+ln(P())λln(P())=ln(D)ln(D);  ln(D),λ=P()P()P()P(),,ln(ww)=ln(w)ln(w)P(,,,)=ln(w)+ln(w)+ln(w)+ln(w)+ln(D)ln(w),P(,,,).P(正常|财务,银行卡,公司,月报) = \frac{P(财务|正常)P(银行卡|正常)P(公司|正常)P(月报|正常)P(正常)}{P(财务)P(银行卡)P(公司)P(月报)} \\ P(垃圾|财务,银行卡,公司,月报) = \frac{P(财务|垃圾)P(银行卡|垃圾)P(公司|垃圾)P(月报|垃圾)P(垃圾)}{P(财务)P(银行卡)P(公司)P(月报)}\\ 其中, P(财务|正常) = \frac{w_{财务词汇数}}{w_{正常文档的词汇数}}\\ P(正常) = \frac{D_{正常文档数}}{D_{总文档数}}\\ 取对数操作\\ \ln(P(财务|正常)) + \ln(P(银行卡|正常)) + \ln(P(公司|正常)) + \ln(P(月报|正常)) +\ln(P(正常)) - \lambda\\ \ln(P(正常)) = \ln(D_{正常文档数}) - \ln(D_{文档总数});\; \ln(D_{文档总数})计算过程不需要用到, 可以忽略\\ \lambda = P(财务)P(银行卡)P(公司)P(月报), 计算过程不需要用到, 可以忽略\\ \ln(\frac{w_{财务}}{w_{正常文档的词汇数}}) = \ln(w_{财务}) - \ln(w_{正常文档的词汇数}) \\\\ P(正常|财务,银行卡,公司,月报) = \ln(w_{财务}) + \ln(w_{银行卡}) + \ln(w_{公司}) + \ln(w_{月报}) + \ln(D_{正常文档数}) - \sum\ln(w_{正常文档的词汇数})\\\\ 同理, P(垃圾|财务,银行卡,公司,月报) 的计算也是类似的.

从计算过程可以看到, 关于整体的多个计算步骤可以省略掉, 最终计算时, 只需要得到类别文档数, 各个词出现次数和在不同类别文档的词汇次总数, 即可计算出不同类别下的概率, 得到概率最大的类别即为文档所属.

2.1 分词

分词相当于基础性的工作, 很大程度影响模型的表现. 由于中文分词和英文利用天然的空格进行不同, 模型的预测结果可能会受到分词的较大影响.

这意味着中文分词的预处理阶段变得很重要, 如何让文本变得更为整洁干净, 能够有效分离各种内容.

js原生分词api的分词效果一般, 但好在执行速度非常快, 由于只是过滤相对特定的目标内容, 对于分词的质量要求不高, 虽然有一些库的分词效果很好, 但执行速度过慢, 所以原生分词api可以满足需要.

2.2 词向量化

from sklearn.feature_extraction.text import CountVectorizer

contents = [
    'this is a test',
    'this is another test',
    'this is yet another test',
    'this is the last test',
]
cv = CountVectorizer()
wv = tf.fit_transform(contents)

wv.toarray()

array([[0, 1, 0, 2, 0, 1, 0],
       [1, 1, 0, 1, 0, 1, 0],
       [1, 1, 0, 1, 0, 1, 1],
       [0, 1, 2, 1, 1, 1, 0]], dtype=int64)

array(['another', 'is', 'last', 'test', 'the', 'this', 'yet'],
      dtype=object)

以上和下面的代码等价.

from collections import Counter

words = [a for a in set(z for e in contents for z in e.split()) if len(a) > 1]

words.sort()

['another', 'is', 'last', 'test', 'the', 'this', 'yet']

data = []
for e in contents:
    tmp = []
    c =Counter(z for z in e.split())
    for k in words:
        tmp.append(c.get(k, 0))
    if tmp:
        data.append(tmp)

data
[[0, 1, 0, 2, 0, 1, 0],
 [1, 1, 0, 1, 0, 1, 0],
 [1, 1, 0, 1, 0, 1, 1],
 [0, 1, 2, 1, 1, 1, 0]]

通常还有另一种处理方式, 不记录出现的次数, 只需要知道某个词是否在文档中出现即可:

cv = CountVectorizer(binary=True)

array([[0, 1, 0, 1, 0, 1, 0],
       [1, 1, 0, 1, 0, 1, 0],
       [1, 1, 0, 1, 0, 1, 1],
       [0, 1, 1, 1, 1, 1, 0]], dtype=int64)

除此之外, 还有TD_IDF.

from sklearn.feature_extraction.text import TfidfVectorizer

由于这里不使用, 不做讨论.

三. MultinomialNB

class sklearn.naive_bayes.MultinomialNB(*, alpha=1.0, force_alpha=True, fit_prior=True, class_prior=None)

alpha, float or array-like of shape (n_features,), default=1.0. 拉普拉斯平滑参数

Additive (Laplace/Lidstone) smoothing parameter (set alpha=0 and force_alpha=True, for no smoothing).

force_alphabool, default=True, 强制使用拉普拉斯, 假如不设置, 参数将被设置为1e-10

If False and alpha is less than 1e-10, it will set alpha to 1e-10. If True, alpha will remain unchanged. This may cause numerical errors if alpha is too close to 0.

New in version 1.2.

Changed in version 1.4: The default value of force_alpha changed to True.

fit_prior, bool, default=True, 是否学习类先验概率. 如果为false, 将使用均匀分布的先验.

Whether to learn class prior probabilities or not. If false, a uniform prior will be used.

class_prior array-like of shape (n_classes,), default=None, 手动指定类先验, 如果指定, 将不会根据数据自动调整

Prior probabilities of the classes. If specified, the priors are not adjusted according to the data.

sklearn MultinomialNB的代码很简单, 这里只取出其中的主要部分, 具体可以查看源代码文件.

上面的推导计算过程和代码一起查看:

P(,,,)=ln(w)+ln(w)+ln(w)+ln(w)+ln(D)ln(w)P(i)=i=1nln(i)i=1nln()+ln()P(正常|财务,银行卡,公司,月报) = \ln(w_{财务}) + \ln(w_{银行卡}) + \ln(w_{公司}) + \ln(w_{月报}) + \ln(D_{正常文档数}) - \sum\ln(w_{正常文档的词汇数})\\ P(类别|词汇_i) = \sum_{i = 1} ^ n\ln(类别词汇出现次数_i) - \sum_{i = 1} ^ n\ln(类别文档词汇总次数) + \ln(类别文档数)

上述的数学推导和MultinomialNB的代码的细微差异

smoothed_fc = self.feature_count_ + alpha # 拉普拉斯平滑处理, 词汇出现次数 + 1, 消除0的存在
smoothed_cc = smoothed_fc.sum(axis=1) # 类别下词汇出现总次数
# 计算概率矩阵
self.feature_log_prob_ = np.log(smoothed_fc) - np.log(
    smoothed_cc.reshape(-1, 1)
)

这部分代码对应的数学推导式:

i=1nln(i)i=1nln()\sum_{i = 1} ^ n\ln(词汇出现次数_i) - \sum_{i = 1} ^ n\ln(类别文档词汇总次数)

log_class_count = np.log(self.class_count_)
# empirical prior, with sample_weight taken into account
self.class_log_prior_ = log_class_count - np.log(self.class_count_.sum()) # 这个计算可以忽略, - np.log(self.class_count_.sum())

这部分代码没有省略总文档数部分分内容 - np.log(self.class_count_.sum()).

ln()ln()\ln(类别文档数) - \ln(文档总数)

# 训练
1. 分类的数量
n_classes = Y.shape[1]

2. 计算次数
self._init_counters(n_classes, n_features) # 初始化两个计数矩阵
self._count(X, Y) # 计数

self._update_feature_log_prob(alpha) # 计算log处理后的概率矩阵
self._update_class_log_prior(class_prior=class_prior) # 计算log处理的分类的概率矩阵

def _init_counters(self, n_classes, n_features):
    # 生成两个零矩阵
    self.class_count_ = np.zeros(n_classes, dtype=np.float64)
    self.feature_count_ = np.zeros((n_classes, n_features), dtype=np.float64)

def _count(self, X, Y):
    """Count and smooth feature occurrences."""
    self.feature_count_ += safe_sparse_dot(Y.T, X) # 计数各个类别文档下, 词汇出现的次数
    self.class_count_ += Y.sum(axis=0) # 各个类别的文档数

def _update_feature_log_prob(self, alpha):
    """Apply smoothing to raw counts and recompute log probabilities"""
    smoothed_fc = self.feature_count_ + alpha # 拉普拉斯平滑处理, 词汇出现次数 + 1, 消除0的存在
    smoothed_cc = smoothed_fc.sum(axis=1) # 类别下词汇出现总次数
	# 计算概率矩阵
    self.feature_log_prob_ = np.log(smoothed_fc) - np.log(
        smoothed_cc.reshape(-1, 1)
    )

log_class_count = np.log(self.class_count_)
# empirical prior, with sample_weight taken into account
self.class_log_prior_ = log_class_count - np.log(self.class_count_.sum()) # 这个计算可以忽略, - np.log(self.class_count_.sum())

# 返回test_text的概率
def _joint_log_likelihood(self, X):
    """Calculate the posterior log probability of the samples X"""
    return safe_sparse_dot(X, self.feature_log_prob_.T) + self.class_log_prior_

jll = self._joint_log_likelihood(X) # 计算概率
[[-34.58139822 -35.17308172]]
return self.classes_[np.argmax(jll, axis=1)] # 返回概率最大值所属类别
import numpy as np
from sklearn.naive_bayes import MultinomialNB

rng = np.random.RandomState(1)
X = rng.randint(5, size=(6, 6))
y = np.array([1, 1, 0, 0, 0, 1])

clf = MultinomialNB()
clf.fit(X, y)
print(clf.predict(X[2:3]))

# jll = self._joint_log_likelihood(X)
[[-34.58139822 -35.17308172]]

js中使用矩阵相对麻烦, 为了方便数据的存储和计算, 将上述矩阵计算形式转为普通的计算方式, 这两部分代码的转换并不复杂(根据上面的数学推导按部就班).

import numpy as np

rng = np.random.RandomState(1)
X = rng.randint(5, size=(6, 6))
y = np.array([1, 1, 0, 0, 0, 1])

normal_dic = {}
for e in X[[0, 1, 5]]:
    for i, z in enumerate(e):
        if normal_dic.get(i):
            normal_dic[i] += z
        else:
            normal_dic[i] = z + 1

bad_dic = {}
for e in X[[2, 3, 4]]:
    for i, z in enumerate(e):
        if bad_dic.get(i):
            bad_dic[i] += z
        else:
            bad_dic[i] = z + 1

# 3 代表的是0, 1两个类的数量, 6 是总数
cnp = np.log(3) - np.log(6)
cbp = np.log(3) - np.log(6)

ac = cnp
bc = cbp
for i, e in enumerate(X[2:3].flatten()):
    a = np.log(normal_dic.get(i)) - np.log(36) # 36, normal类目下词汇出现的总次数
    b = np.log(bad_dic.get(i)) - np.log(45) # 45, bad类目下词汇出现的总次数
    ac += a * e
    bc += b * e

print(ac, bc)
-35.173081722827426
-34.581398221080526

3.1 . JavaScript

由于第一次写这个算法代码时, 对于贝叶斯文本分类上的一些理解偏差(习惯了python开箱即用, 徒手实现代码还是容易犯小错误的), 导致脚本的贝叶斯算法有些小问题(逻辑是没问题的), 但是在实际的表现中, 算法表现出很好的效果, 暂未对脚本的算法进行修改.

{
    const GM_getValue = (...args) => null;
    const GM_setValue = (...args) => null;
    class Bayes_Module {
        #segmenter = null;
        #abc_reg = /[a-z]+/ig;
        #num_reg = /[0-9]+/g;
        #year_reg = /20[0-2][0-9]|12306/g;
        #clear_reg = /[a-z0-9\s]+/ig;
        #white_list = [
            '开始是生成器, 看完就变协程了',
            '99%的教程都没讲明白的yieldfrom有那么难嘛? |PythonAsyncIO从入门到放弃06',
            'Python迭代器深入讲解|【AsyncIO从入门到放弃#1】',
            '回调未来|PythonAsyncIO从入门到放弃09',
            '自制EventLoop|PythonAsyncIO从入门到放弃08',
            '这也许是asyncio中最关键的一行代码|PythonAsyncIO从入门到放弃07',
            '99%的教程都没讲明白的yieldfrom有那么难嘛? |PythonAsyncIO从入门到放弃06',
            '原来yield要这样用才叫真正的协程|PythonAsyncIO从入门到放弃05',
            '明明是生成器, 却偏说是协程, 你是不是在骗我? |PythonAsyncIO从入门到放弃04',
            '开始是生成器, 看完就变协程了',
            'Python用yield关键字定义生成器【AsyncIO从入门到精通#2】',
            'Python迭代器深入讲解|【AsyncIO从入门到放弃#1】',
            'docker初印象|零基础快速入门',
            'Python装饰器实战技巧|Python进阶',
            '写Python装饰器的套路|Python进阶',
            'Python函数参数|深入理解*args和**kwargs|Python进阶',
            '使用minikube快速搭建Kubernetes集群|v1.7.3',
            'GameofPODs-Kubernetes|看完这个, 你想学K8s了么',
            '初学python者最容易产生的误解|无废话5分钟快速解释',
            'VirtualBox上的OpenStack如何调通外部网络|使用Kolla搭建',
            '详解Python命名空间|全局变量和局部变量和自由变量|Python进阶',
            '【秒懂】5分钟学会SSH端口转发, 远程工作用得着|如何充分利用云服务器',
            '10个例子, 刷新你对Python变量的认知|使用Thonny解剖式讲解|Python入门到进阶',
            '安装不算完事, 只有理解了虚拟环境才算真正掌握Python环境',
            '善用帮助和文档, Python自学不求人',
            '尝试用Python写病毒仿真程序',
            '比IDLE好用, 这款Python初学者专属IDEThonny值得拥有',
            '1分钟设置pip镜像源, 不用费心去记配置文件放在哪',
            'Python迭代器深入讲解|【AsyncIO从入门到放弃#1】',
            'Python用yield关键字定义生成器【AsyncIO从入门到精通#2】',
            '开始是生成器, 看完就变协程了',
            '明明是生成器, 却偏说是协程, 你是不是在骗我? |PythonAsyncIO从入门到放弃04',
            '原来yield要这样用才叫真正的协程|PythonAsyncIO从入门到放弃05',
            '全网最好的Python进阶课程-Python3: 深入探讨( 序列, 迭代器, 生成器, 上下文管理器) 中英字幕',
            '【中英字幕】2023哈弗大学CS50Python&JavaScriptWeb开发课程-14个小时完整版',
            '5个小时PySide6完全开发指南使用Qt进行PythonGUI桌面应用开发( 中英字幕) ',
            '【YouTube热门+中文字幕】Python开发者学习Rust最佳路径-FromPythontoRust',
            '2022Python教程: Simplilearn10小时Python完整入门课程( 中英字幕) ',
            'Python区块链开发教程-Solidity, 区块链和智能合约核心概念及全栈开发课程( 中英文字幕) ',
            '【Udemy2022Python超级课程】通过构建10个基于现实世界的应用程序-学习Pytho核心技能( 中英文字幕) 下',
            '【Udemy2022Python超级课程】通过构建10个基于现实世界的应用程序-学习Pytho核心技能( 中英文字幕) 上',
            '【Udemy付费课程】PythonNLP自然语言处理( SpaCy, NLTK, Sklearn, CNN) 和8实践个项目( 中英文字幕) ',
            '【Udemy高分付费课程】2022Python数据科学和机器学习训练营-Tensorflow, 深度学习, 神经网络, 回归分类, 人工智能( 中英文字幕) ',
            '【Udemy高分付费课程】Python数据结构与算法-终极Python编码面试和计算机科学训练营( 中英文字幕) ',
            '【Udemy高分Python机器学习课程】2022完整训练营-使用Tensorflow, Pandas进行Python机器学习( 中英文字幕) 下',
            '【Udemy高分Python机器学习课程】2022完整训练营-使用Tensorflow, Pandas进行Python机器学习( 中英文字幕) 上',
            '【UdemyPython机器学习】在Python中学习机器学习, 深度学习, 贝叶斯学习和模型部署( 中英文字幕) ',
            '【Udemy排名第一的Python课程】2022PythonPRO训练营-100天构建100个Python项目成为实战专家! ( 中英文字幕) P3',
            '【Udemy排名第一的Python课程】2022PythonPRO训练营-100天构建100个Python项目成为实战专家! ( 中英文字幕) P2',
            '【Udemy排名第一的Python课程】2022PythonPRO训练营-100天构建100个Python项目成为实战专家! ( 中英文字幕) P1',
            '【Mosh1个小时入门Python】PythonforBeginners-LearnPythonin1Hour( 中英文字幕) ',
            '【YouTube百万粉丝大神Mosh】Python系列完整教程( 中英文字幕) ',
            '【Udemy付费课程】使用PyQt6和Qt设计器进行PythonGUI开发( 中英文字幕) ',
            '【Udemy付费课程】Python机器学习和Python深度学习与数据分析, 人工智能, OOP和Python项目( 中英文字幕) ',
            '【油管BroCode】面向初学者的Python基础入门教程-->Pythontutorialforbeginners-->( 中英文字幕) ',
            '【Udemy付费课程】RESTAPIswithFlaskandPython-->Flask基础课程( 中英文字幕) ',
            '【Udemy付费课程】Django4andPythonFull-StackDeveloperMasterclass->Python全栈开发者大师课',
            '【Udemy付费课程】AdvancedRESTAPIswithFlaskandPython-->PythonFlask进阶课程( 中英文字幕) ',
            '【Udemy付费课程】PythonDjango2021-CompleteCourse-->PythonDjango开发指南( 中英文字幕) ',
            '【Udemy付费课程】PythonforAbsoluteBeginners-->面向初学者的Python入门教程( 中英文字幕) ',
            '用Python从视频里面扒PPT? ',
            '2023年, 我在用哪些VSCODE插件? ',
            '流畅的Python',
            '【Python进阶】Py-spy- 最佳性能分析工具, 你的程序到底慢在哪',
            '「Python」VSCode如何搭建Python开发环境? VSCode如何运行Python代码',
            '「Python」什么是变量? 如何定义变量? 如何为变量赋值',
            '「Python」什么是数据类型? 数字, 字符串与布尔类型介绍',
            '「Python」基础教程什么是列表和元组? 列表和元组的书写格式以及区别',
            '「Python」基础教程什么是字典? 字典的书写格式',
            '「Python」基础教程什么是集合? 集合和列表的区别',
            '「Python」基础教程什么是条件控制语句? if语句的书写格式',
            '「Python」基础教程什么是循环语句? while语句的书写格式, 如何跳出while循环',
            '「Python」基础教程循环语句for的书写格式, for语句可以遍历的目标有哪些',
            '「Python」基础教程函数有什么作用? 如何定义函数',
            '「Python」进阶教程什么是模块? 如何创建导入和使用模块',
            '「Python」进阶教程类有什么作用? 如何定义和使用类',
            '「Python」进阶教程什么是构造方法? 构造方法__init__以及参数self的作用',
            '「Python」进阶教程类的方法有什么作用? 如何定义和调用类的方法',
            '「Python」进阶教程什么是类的继承? 继承的作用, 如何实现类的继承',
            '「Python」进阶教程方法重写有什么用? 如何实现类的方法重写? 使用super访问父类',
            '「Python」进阶教程什么是文件? 如何读取写入文件? 文件读写相关函数介绍',
            '「Python」进阶教程什么是异常? 如何处理异常? tryexcept语句的书写格式',
            '「Python」进阶教程随机数有什么用? 如何生成随机数? random和randint的区别',
            '「Python」进阶教程日期时间有什么用? 如何获取当前日期时间',
            '「Python」高级教程什么是内部函数? 内部函数的作用, 如何定义内部函数',
            '「Python」高级教程什么是内部类? 如何定义和使用内部类',
            '「Python」高级教程什么是变量的作用域? 变量的作用范围和检索顺序',
            '「Python」高级教程函数和方法如何修改模块变量? 关键字global的作用以及书写格式',
            '「Python」高级教程如何修改上一级变量? 关键字nonlocal的作用以及书写格式',
            '「Python」高级教程类的私有成员的作用是什么? 如何定义类的私有字段和方法',
            '「Python」高级教程类的静态字段的作用是什么? 如何定义和使用类的静态字段',
            '「Python」高级教程类和实例访问静态字段的区别是什么? 实例修改静态字段陷阱',
            '「Python」高级教程如何定义和调用类的静态方法? staticmethod与classmethod的区别',
            '「Python」高级教程什么是JSON? JSON的书写格式, JSON与Python对象之间的转换',
            '【python】字节码和虚拟机? python代码竟然是这么执行的! ',
            '【python】B站没人讲过的CodeObject, python底层实现一点都不简单! ',
            '【python】python的骨架frame- - 你写的代码都是运行在它里面的? ',
            '【python】看似简单的加法, 背后究竟有多少代码需要运行? 看了才知道! ',
            '【python】天使还是魔鬼? GIL的前世今生. 一期视频全面了解GIL! ',
            '【python】你知道描述器是多么重要的东西嘛? 你写的所有程序都用到了! ',
            '【python】装饰器超详细教学, 用尽毕生所学给你解释清楚, 以后再也不迷茫了! ',
            '【python】一个公式解决所有复杂的装饰器, 理解了它以后任何装饰器都易如反掌! ',
            '【python】如何在class内部定义一个装饰器? 这里的坑你要么不知道, 要么不会填! ',
            '【python】生成器是什么? 怎么用? 能干啥? 一期视频解决你所有疑问! ',
            '【python】对迭代器一知半解? 看完这个视频就会了. 涉及的每个概念, 都给你讲清楚! ',
            '【python】闭包的实现机制. 嵌套函数怎么共享变量的? ',
            '【python】python中什么会被当真? 你知道if判断背后的规则吗? ',
            '【python】你知道定义class背后的机制和原理嘛? 当你定义class的时候, python实际运行了什么呢? ',
            '【python】你知道MRO是什么嘛? 你知道多继承的顺序是怎么决定的嘛? 你知道这个视频是B站最硬核的MRO教程嘛? ',
            '【python】class里定义的函数是怎么变成方法的? 函数里的self有什么特殊意义么? ',
            '【python】metaclass理解加入门, 看完就知道什么是元类了. ',
            '【python】__slots__是什么东西? 什么? 它还能提升性能? 它是如何做到的! ? ',
            '【python】B站最细致的super()详解, 一定有你不知道的知识! ',
            '【python】staticmethod与classmethod深度机制解析- - 要知其所以然',
            '【python】加俩下划线就私有了? 聊聊python的私有变量机制. 为什么说它不是真的私有变量? ',
            '【python】定义好的变量读不出来? 详解全局变量和自由变量的使用! ',
            '【python】TypeHint入门与初探, 好好的python写什么类型标注? ',
            '【python】TypeHint的进阶知识, 这下总该有你没学过的内容了吧? ',
            '【python】Python的N种退出姿势, 你都了解嘛? 一期视频让你把每种方法都搞清楚! ',
            '【python】你听说过namedtuple嘛? 会用嘛? 知道它实现的原理嘛? ',
            '【python】mutable和immutable其实根本没区别? 带你了解这个概念背后你没思考过的东西',
            '【python】内存管理结构初探- - 我要的内存从哪儿来的? ',
            '【python】Unreachable的对象咋回收的? generation又是啥? ',
            '【python】和python开发人员用同一套命名系统, 一期视频就学会! ',
        ];
        #black_list = [
            '【Python爬虫】用Python爬取各大平台VIP电影, 不花钱也能享受付费一般的待遇, 这不轻轻松松? ',
            'PyCharm安装激活教程, 一键使用永久激活, 新手宝宝可以直接入手! ! ! ',
            '【python自动化】用python代码写一个前端打地鼠游戏, 精准自动打地鼠机器人, 边学边玩! ',
            '【Python爬虫】教你用Python爬取漫画资源, Python批量下载付费漫画, 实现免费阅读, 永久白嫖! ! ',
            '【Python爬虫】一分钟教你追剧看电影不求人! python爬虫代码一分钟教你爬取各平台电影视频, 小白也能学会! ',
            '【Python爬虫】教你用Python爬取网易云音乐免费听音乐, 实现听歌自由, 批量下载付费音乐, 源码可分享! ',
            '【Python自动化脚本】用Python实现办公自动化, 一键生成PPT演示文稿( 源码可分享) 步骤简单, 轻松上手! ',
            '【Python爬虫】毕业生学习项目教你用Python爬虫爬取百度文库vip资源, 操作简单, 有手就会( 附源码) ! ! ! ',
            '【Python自动化脚本】Python实现OCR识别提取图片文字, 多语言支持, 操作简单新手小白也能学会, 附源码! ! ! ',
            '【Python爬虫】用Python爬取各大平台VIP电影, 不花一分钱也能享受付费的待遇, 妈妈再也不用但心乱花钱了',
            '【提供源码】教你用Python爬取知网数据, 批量下载论文摘要! 步骤简单, 小白也能学会! Python爬虫/中国知网',
            '【Python游戏】用20行Python代码, 制作不一样的超级玛丽游戏, 手把手教学, 制作简单, 小白也能学会! ! ',
            '【Python自动化】Python自动答题辅助脚本! python代码实现快速答题, 在线考试, 正确率100%',
            'Python实现OCR识别提取图片文字, 多语言支持, 步骤简单小白也能学',
            '【python自动化】用python代码写一个前端打地鼠游戏, 精准自动打地鼠机器人, 边学边玩! ',
            '【Python爬虫】两分钟教你用Python爬取漫画资源, Python批量下载付费漫画, 实现免费阅读, 永久白嫖! ! ',
            '【Python爬虫】用Python爬取各大平台VIP电影, 不花钱也能享受付费一般的待遇, 这不轻轻松松? ',
            '【Python爬虫】教你用Python免费听音乐, 实现听歌自由, 批量下载付费音乐, 源码可分享! ',
            'PyCharm安装激活教程, 一键使用永久激活, 新手宝宝可以直接入手! ! ! ',
            '【Python爬虫】2023最新Python安装视频, 一键激活永久使用, 小白必备, 附安装包激活码分享! ',
            '【从0→1】3天搞定Python爬虫, 即学即用! ',
            '【Python函数】Python基础打不打的劳, 这50个函数必须掌握! ! ',
            'Python太牛了, 用代码就是实现电脑自动玩笨鸟先飞游戏',
            '天呐! Python自动玩2048也太变态了吧',
            '【附源码】Python实用小技巧-实现自动获取海量IP',
            '【附源码】Python自动化办公之自动发工资条! ! ',
            '【Python爬虫】Python一分钟白嫖超清vip壁纸, 轻松实现壁纸自由! ',
            'Python实现12306自动抢票, 100%成功, 春节出行无忧! 不用熬夜抢票啦! ',
            '【Python实战】Python还不熟练? 多实战敲敲这个打地鼠游戏吧! ',
            '【附源码】过年了, 不得给好朋友准备一个特别的礼物? ',
            '【Python爬虫】2024了, 是谁还在尬聊啊? Python爬取百度表情包, 分分钟成为表情包大户',
            '【js逆向案例-超简单】百度翻译爬虫逆向',
            '【附源码】Python爬虫实战, 猫眼电影一闪一闪亮星星影评爬虫及可视化',
            '【附源码】Python自动化脚本, 实现微信自动回复',
            'Python自动抢购, 准点秒杀飞天茅台, 过年送礼不愁啦! ',
            '【附源码】Python必练入门实战小项目, 不会还有人不会吧~ ~ ',
            '【附源码】手把手教你800行代码自制蔡徐坤打篮球小游戏! ',
            '【Python游戏】超级牛! 几十行代码就做出了一个【水果忍者】游戏! ',
            '【Python爬虫】Python实现超清4k壁纸下载, 附源码~',
            '【附源码】手把手教你用Python开发俄罗斯方块小游戏_Python练手项目_巩固python基础_Python小游戏',
            '【2024版】Python一分钟破解WiFi密码, 随时随地上网, 根本不缺流量! ',
            '【源码可分享】简单用500行Python代码, 复刻游戏< 我的世界> , 无需插件, 零基础也能轻松上手! ',
            '【大麦网抢票】最新攻略! Python自动购票脚本, 各大演唱会门票轻松购~',
            '【附源码】Python实现12306自动抢票! 寒假不愁, 出行无忧! ',
            '【附源码】小说党福音! 一分钟暴力爬取各平台VIP小说, 快码住',
            '【python学习】给所有python人一个忠告, 普通人学python玩的就是信息差! ! ! ',
            '国内新兴行业已经崛起,真心建议大家冲一冲新兴领域, 工资高不内卷!!!',
            '【python学习】张雪峰: 给所有python人一个忠告, 普通人学python玩的就是信息差! ! ! ',
            '【python大麦抢票】大麦网自动抢票脚本, 原价秒杀门票, 成功率100%! ! ! ',
            '真心的建议大家都冲一冲新兴领域! ! ! 工资高不内卷, 一定要试试哦不然可会后悔的! ! ! ',
            '【python资料】张雪峰: 给所有python人一个忠告, 普通人学python玩的就是信息差! ! ! ',
            '【python资料】python真的没有大家想的那么难, 只要找对方法, 那就会很简单啦! ! ! ',
            '闲着没事在家用python接单, 日均入账300+. 如果你会python不去接单就真的太可惜了! ! ! ',
            '【附源码】教你10秒暴力破解WiFi密码, 蹭WiFi神器, 一键免费连接WiFi, 附安装教程, 源码! ',
            '【python资料】python学不懂? 千万不要自暴自弃! 学姐一招帮你解决所有难题! ! ! ',
            '前景好不内卷的新兴领域崛起, 真心建议大家都冲一下, 千万不要错过了再去后悔! ! ! ',
            '【python资料】听学姐一句劝! 想学好Python, 一定要找到正确的学习方法! ! ! ',
            '新兴领域崛起, 真心建议大家都冲一下! ! ! ',
            '各位确定不冲一冲新兴职业吗? 现在发展可太香了吧! ! ! ',
            '【python代码】2024年最新爱心代码分享, 快@你的那个ta吧! ! ! ',
            '【python资料】python全套学习资料分享, 是时候打开一条正确学习python的道路了! ! ! ',
            '【python代码】手把手教你使用python爬取全网音乐, 附源码! ! ! ',
            '【python资料】适合python小白学习的python全套资料分享, 别再盲目的学习python了',
            '【python代码】植物大战僵尸python代码分享, 大家一起来制作游戏吧! ! ! ',
            '【python软件】分享一个python软件神器, 帮助你解决所有python难题! ! ! ',
            '【python资料】python小白全套资料分享, 再也不要盲目学习python了!!!',
            '【2024清华版Python教程】目前B站最完整的python( 数据分析) 教程, 包含所有干货内容! 这还没人看, 我不更了! ',
            '计算机专业上岸学姐推荐: 编程小白的第一本python入门书, 啃完你的python就牛了! ',
            'B站首推! 字节大佬花一周讲完的Python, 2024公认最通俗易懂的【Python教程】小白也能信手拈来! ( 爬虫|数据分析|Web开发|项目实战) 等等随便白嫖! ',
            '【全268集】北京大学168小时讲完的Python( 数据分析) 教程, 通俗易懂, 2024最新版, 全程干货无废话, 这还学不会, 我退出IT界! ',
            '拜托三连了! 这绝对是全B站最详细的Python学习路线图( 2024新版) 让小白少走弯路! ',
            'python+pycharm安装配置教程( 2024零基础学python教程必看)',
            '吹爆! 适合所有零基础人群的最全Python学习路线, 我给做出来了! -基础语法/爬虫/全栈开发/数据分析/人工智能',
            'Python70个练手项目, 包含爬虫_web开发_数据分析_人工智能等, 练完你的Python就牛了! ',
            '揭秘! 学Python真的能兼职接单吗? 零基础/价格参考/平台推荐/接单技巧/',
            '拜托三连了! 这绝对是全B站最用心( 没有之一) 的Python数据分析-数据挖掘课程, 全程干货无废话, 学完即可就业! ',
            '【Python初学者必定要看的入门神书! 】下载量超5万, 单细胞生物都能看懂! -基础语法/网络爬虫/Web编程/数据分析',
            '拜托三连了! 这绝对是全B站最用心( 没有之一) 的Python爬虫教程, 零基础小白从入门到( 不) 入狱! ',
            'Python爬虫|我宣布:这三本书就是学习Python爬虫的天花板! 都给我磕到烂! ',
            'B站首推! 自学Python一定要看的3本书籍! ! ! 少走三年弯路! ! ! ',
            'Python学习|学Python顺序真的很重要! 千万不要搞反啦千万不要弄反了! ! ! 能少走一年弯路! ',
            'B站首推! 华为大佬168小时讲完的Python( 数据分析) 教程, 全程干货无废话! 学完变大佬! 这还学不会我退出IT界! ',
            '冒死上传( 已被开除) 花八千块在某站买的Python课程, 每天学习1小时, 零基础从入门到精通! ',
            '盲目学习只会毁了你! 这绝对是全B站最用心( 没有之一) 的Python爬虫教程, 整整500集, 从入门到( 不) 入狱, 学完即可兼职接单! ',
            'B站首推! 字节大佬一周讲完的Python【数据分析】教程, 整整300集, 全程干货无废话, 学完即可就业!',
            '【2023版】这绝对是B站最详细的Python+Pycharm安装配置教程, 真正让小白少走弯路, 激活码允许白嫖! ',
            '【整整300集】北京大学198小时讲完的人工智能课程( 机器学习_深度学习_OpenCV_神经网络等) 全程干货无废话, 学完立马变大神! ',
            '【整整600集】北京大学198小时讲完的Python教程( 数据分析) 全程干货无废话! 学完变大佬! 这还学不会, 我退出IT圈! ',
            'B站首推! 华为团队花一周讲完的人工智能, 2023公认最通俗易懂的【AI人工智能教程】小白也能信手拈来( |机器学习|深度学习|芯片) 等等随便白嫖! ',
            '【整整300集】暑假60天如何逼自己学会Python, 从入门到精通, 每天坚持打卡练习, 学不会我退出IT界! ',
            '【2023清华版Python教程】可能是B站最好的Python教程, 全300集包含入门到实战所有干货, 存下吧, 很难找全的! ',
            '【全600集】我花3W买的Python系课统, 让你少走99%的弯路! 手把手教学, 通俗易懂, 零基础快速进阶Python大佬! 学完即可就业! 不会我退出IT教学圈! ',
            '【浙江大学亲授】B站最系统的Python数据分析教学, 整整300集, 包含数据获取, 分析, 处理, 挖掘等, 小白从入门到项目实战保姆级教程, 学完即可就业, 存下吧! ',
            '【全800集】少走99%的弯路! 清华大佬耗费一周录制的Python教程, 手把手教学, 通俗易懂!0基础小白快速进阶大神, 无私分享, 拿走不谢! 还不快来学起来! ',
            '【Python教程】华为大佬花一周讲完的Python教程, Python从入门到精通, 包括基础教程, 案例教学, 进阶学习和全流程实战, 整整400集, 熟练掌握并运用! ',
            'Python教程|100个Python新手小白必备的练习题, 简单又实用, 手把手教学, 每日一练, 轻松掌握, 实践是检验真理的唯一标准! ',
            '【Python系统课程】268个小时讲完的付费Python系统教程, 花了3W买的, 无私分享, 整整500集! 包含基础, 核心编程和爬虫, 数据分析, 学完即可就业! ',
            '【整整500集】B站最系统的Python爬虫教程, 从入门到入狱! 保姆级手把手教学, 全程干货无废话, 学完即可就业, 别在盲目自学了! ! ! ',
            '【Python零基础教程】可能是B站最系统的Python教程, 一周时间全面了解python从入门到精通, 包含所有干货, 少走99%的弯路, 学完即可就业! ',
            '【全500集】清华大佬终于把Python教程做成了漫画书, 结合漫画元素讲解, 通俗易懂, 全程干货无废话, 学完即可就业, 拿走不谢! 这还学不会我退出IT圈! ',
            '【Python教程】这才是你需要学的! 一套针对零基础的python教程, 整整300集, 全程干货无废话, python从入门到精通, 存下吧, 很难找全的! ',
        ];
        #black_p = 0;
        #white_p = 0;
        #white_len = 0;
        #black_len = 0;
        #total_len = 0;
        #black_counter = null;
        #white_counter = null;
        /**
         * 分词
         * @param {string} content
         * @param {number} exclude_length
         * @returns {Array}
         */
        #seg(content, exclude_length = 1) { return [...this.#segmenter.segment(content)].map(e => e.segment).filter(e => e.length > exclude_length); }
        /**
         * 统计词频
         * @param {Array} words_list
         * @returns {object}
         */
        #word_counter(words_list) { return words_list.reduce((counter, val) => (counter[val] ? ++counter[val] : (counter[val] = 1), counter), {}); }
        /**
         * 手动规则取词
         * @param {string} content
         */
        #seg_word(content) {
            const words = [];
            const mabc = content.match(this.#abc_reg);
            mabc && words.push(...mabc.filter(e => e.length > 1).map(e => e.toLowerCase()));
            const mnum = content.match(this.#num_reg);
            mnum && words.push(...mnum.filter(e => this.#year_reg.test(e)));
            words.push(...this.#seg(content.replace(this.#clear_reg, ''), 0));
            return words;
        }
        #get_word_counter() {
            this.#black_counter = GM_getValue('black_counter');
            this.#white_counter = GM_getValue('white_counter');
        }
        /**
         * 计算先验概率
         * @param {number} black_len
         * @param {number} white_len
         * @param {number} total_len
         */
        #get_prior_probability(black_len, white_len, total_len) {
            this.#black_p = Math.log((black_len + 1) / total_len + 2);
            this.#white_p = Math.log((white_len + 1) / total_len + 2);
        }
        #init_module() {
            let total_len = GM_getValue('total_len'), white_len = 0, black_len = 0;
            if (total_len) {
                white_len = GM_getValue('white_len');
                black_len = GM_getValue('black_len');
                this.#get_word_counter();
            } else {
                white_len = this.#white_list.length;
                black_len = this.#black_list.length;
                total_len = white_len + black_len;
                GM_setValue('white_len', white_len);
                GM_setValue('black_len', black_len);
                GM_setValue('total_len', total_len);
                this.#black_counter = this.#word_counter(this.#black_list.map(e => this.#seg_word(e)).flat());
                this.#white_counter = this.#word_counter(this.#white_list.map(e => this.#seg_word(e)).flat());
                GM_setValue('black_counter', this.#black_counter);
                GM_setValue('white_counter', this.#white_counter);
            }
            this.#white_len = white_len;
            this.#black_len = black_len;
            this.#total_len = total_len;
            this.#get_prior_probability(black_len, white_len, total_len);
        }
        constructor() {
            this.#segmenter = new Intl.Segmenter('cn', { granularity: 'word' });
            this.#init_module();
        }
        /**
         * 计算概率
         * @param {string} content
         * @returns {boolean}
         */
        bayes(content) {
            const c = this.#seg_word(content);
            let wp = this.#white_p, bp = this.#black_p;
            c.forEach(e => {
                const bc = this.#black_counter[e] || 0;
                const wc = this.#white_counter[e] || 0;
                // 拉普拉斯平滑处理, 避免 0 概率
                wp += Math.log((wc + 1) / (this.#white_len + 2)); // 这个地方写错, this.#white_len, this.#black_len
                bp += Math.log((bc + 1) / (this.#black_len + 2)); // 应该是类别的词汇出现次数总和而不是类别文档数
            });
            console.log(bp, wp);
            return (bp - wp) / Math.abs(bp) > 0.5; // 当数值差距达到一定程度时, 才做出判断
        }
        /**
         * 添加新内容
         * @param {string} content
         * @param {boolean} mode
         */
        add_new_content(content, mode) {
            const ws = this.#seg_word(content);
            const [dic, dic_name, len_name, len_data] = mode ? [this.#white_counter, 'white_counter', 'white_len', ++this.#white_len] : [this.#black_counter, 'black_counter', 'black_len', ++this.#black_len];
            ws.forEach(e => dic[e] ? ++dic[e] : (dic[e] = 1));
            this.#total_len += 1;
            this.#get_prior_probability()
            GM_setValue('total_len', this.#total_len);
            GM_setValue(dic_name, dic);
            GM_setValue(len_name, len_data);
        }
    }
    console.time()
    const bay = new Bayes_Module();
    // debugger;
    console.log(bay.bayes('【整整600集】清华大学196小时讲完的Python教程( 数据分析) 零基础入门到精通全套教程, 全程干货无废话, 这还学不会, 我退出IT圈! 数据挖掘/可视化/大数据'));
    console.timeEnd()
}

-89.81575826467702 -160.12904835407699
VM48:350 true
VM48:351 default: 10.932861328125 ms

由于进行判别的文本长度一般不会很长, 为了避免误过滤, 进行分词时将保留绝大部分的内容, 包括各种符号(只剔除掉部分数字)都将用于作为特征参数.

四. ComplementNB

补充一下这部分的内容:

补集贝叶斯, 多项式的稍微改进(注意这里, 就是稍微), 基本的计算和多项式基本一样, 只是在计算上有轻微的差异.

sklearn.naive_bayes.ComplementNB - scikit-learn 1.4.2 documentation

由于代码和多项式基本一样的, 这里就只截取重要部分的内容

def _count(self, X, Y):
        """Count feature occurrences."""
        check_non_negative(X, "ComplementNB (input X)")
        self.feature_count_ += safe_sparse_dot(Y.T, X)
        self.class_count_ += Y.sum(axis=0)
        self.feature_all_ = self.feature_count_.sum(axis=0)

def _update_feature_log_prob(self, alpha):
    """Apply smoothing to raw counts and compute the weights."""
    comp_count = self.feature_all_ + alpha - self.feature_count_
    logged = np.log(comp_count / comp_count.sum(axis=1, keepdims=True))
    # _BaseNB.predict uses argmax, but ComplementNB operates with argmin.
    if self.norm:
        summed = logged.sum(axis=1, keepdims=True)
        feature_log_prob = logged / summed
      else:
        feature_log_prob = -logged               # 注意这里进行了负值操作, 因为需要取最小值, 而代码统一使用的时最大值, 所以加个符号
    self.feature_log_prob_ = feature_log_prob

def _joint_log_likelihood(self, X):
    """Calculate the class scores for the samples in X."""
    jll = safe_sparse_dot(X, self.feature_log_prob_.T)
    if len(self.classes_) == 1:
        jll += self.class_log_prior_
    return jll

可以看到 _update_feature_log_prob, _joint_log_likelihood这两个函数有所差异

def _update_feature_log_prob(self, alpha):
    """Apply smoothing to raw counts and recompute log probabilities"""
    smoothed_fc = self.feature_count_ + alpha # 拉普拉斯平滑处理, 词汇出现次数 + 1, 消除0的存在
    smoothed_cc = smoothed_fc.sum(axis=1) # 类别下词汇出现总次数
	# 计算概率矩阵
    self.feature_log_prob_ = np.log(smoothed_fc) - np.log(
        smoothed_cc.reshape(-1, 1)
    )

def _joint_log_likelihood(self, X):
    """Calculate the posterior log probability of the samples X"""
    return safe_sparse_dot(X, self.feature_log_prob_.T) + self.class_log_prior_ # 这部分被去掉

关键的部分

self.feature_count_ += safe_sparse_dot(Y.T, X)
self.class_count_ += Y.sum(axis=0)

self.feature_all_ = self.feature_count_.sum(axis=0) # 把这两部分合在一起看, 就会发现这两部分的计算是多余的, 因为两组数加起来
comp_count = self.feature_all_ + alpha - self.feature_count_ # 然后又分别减去其中一组数, 实际等于没有操作, 可以省略操作, 但是源码进行其中的计算还有一些其他计算的考量, 但是在这里不起作用, 就是个重复步骤

logged = np.log(comp_count / comp_count.sum(axis=1, keepdims=True))

上面的矩阵不好阅读, 翻译成普通代码.

total_dic = {}
for k, v in normal_dic.items():
    total_dic[k] = v + bad_dic[k]

ac = 0
bc = 0
bs = sum(v for v in bad_dic.values()) + 6
a_s = sum(v for v in normal_dic.values()) + 6
for i, e in enumerate(X[2:3].flatten()):
	# np.log(total_dic.get(i) + 1 - normal_dic.get(i)) - np.log(bs)
    b =  np.log(bad_dic.get(i) + 1) - np.log(bs)
    a =  np.log(normal_dic.get(i) + 1) - np.log(a_s)
    ac += a * e
    bc += b * e
cnp = 0 # np.log(3) - np.log(6)
cbp = 0 # np.log(3) - np.log(6)

ac = cnp
bc = cbp
for i, e in enumerate(X[2:3].flatten()):
    a = np.log(normal_dic.get(i)) - np.log(a_s) # 36, normal类目下词汇出现的总次数
    b = np.log(bad_dic.get(i)) - np.log(bs) # 45, bad类目下词汇出现的总次数
    print(a, b)
    ac += a * e
    bc += b * e

print(ac, bc)

注释掉多项式中的先验概率(即两组的先验都假设为0), 两部分的代码上下对比会发现计算值的绝对值是一样的,.

rng = np.random.RandomState(1)
X = rng.randint(5, size=(6, 6))
y = np.array([1, 1, 0, 0, 0, 1])

normal_dic = {}
for e in X[[0, 1, 5]]:
    for i, z in enumerate(e):
        if normal_dic.get(i):
            normal_dic[i] += z
        else:
            normal_dic[i] = z

bad_dic = {}
for e in X[[2, 3,4]]:
    for i, z in enumerate(e):
        if bad_dic.get(i):
            bad_dic[i] += z
        else:
            bad_dic[i] = z

# 这部分代码是多余的
total_dic = {}
for k, v in normal_dic.items():
    total_dic[k] = v + bad_dic[k]

ac = 0
bc = 0
bs = sum(v for v in bad_dic.values()) + 6
a_s = sum(v for v in normal_dic.values()) + 6
for i, e in enumerate(X[2:3].flatten()):
    b =   np.log(bad_dic.get(i) + 1) - np.log(bs)
    a =   np.log(normal_dic.get(i) + 1) - np.log(a_s)
    ac += a * e
    bc += b * e

print(ac, bc)

img

这就是前面所说的两个模型之间的差异就是一点点, 即先验概率的设置问题.

img

但是文档给出的解析就看起来有点复杂, 并没有代码来得直接...注意数学模型中是求最小值, 代码为了统一取最大值, 加个负号即可.

补集贝叶斯为什么会将先验概率设置为0, 0(或者认为不同分类的先验都是相等的), 这对于不均衡样本有什么影响呢?

img

当不同样本数量差异过大时, 先验概率之间会产生巨大的差异, 可能导致后面的特征值计算的概率值起不到足够作用.

还有一个需要注意的, 补集贝叶斯的稳定性问题, 多数情况下还是以多项式为主.

五. 小结

在这种**"规律明显"**的垃圾内容上, 只需要往词库添加一条标题内容, 即可以实现对绝大部分类似内容的过滤, 对于固定规则而言是很难做到的, 这就是统计方法的力量.

img

需要注意, 虽然贝叶斯在文本分类表现很好, 但是这些和建立的词库和分词关系很大, 为了避免误过滤, 可以调整判断的标准, 例如比较得到的两组概率数值大大小, 当差值达到一定的范围才做出判断.