观文听傑

返回

上一篇的 RBF 支持向量机(Support Vector Machine,SVM)直接寻找分类边界:哪些样本靠近边界、边界怎样弯曲,是训练的中心。但在垃圾邮件过滤中,我们经常面对数万维词表、许多零计数和很少的标注样本。先学习一条复杂边界未必是最经济的起点。

另一条路线是先问:垃圾邮件通常会产生哪些词?正常邮件又会产生哪些词?收到新邮件后,再反过来判断哪一类更可能生成它。这就是生成式分类(Generative Classification)的思路。

本文只讲透三个紧密环节:类别先验与词频似然怎样组成后验分数、条件独立假设怎样把高维联合概率拆开,以及平滑与对数空间怎样让计算真正可用。

01 为什么从“边界”转向“数据怎样生成”?#

设训练集有 NN 封邮件,词表包含 VV 个词。把第 ii 封邮件写成词频向量:

xi=[xi1,xi2,,xiV]N0Vx_i=[x_{i1},x_{i2},\ldots,x_{iV}]\in\mathbb{N}_0^V
  • xijx_{ij}:第 ii 封邮件中词 jj 出现的次数;
  • XN0N×VX\in\mathbb{N}_0^{N\times V}:全部训练邮件的稀疏计数矩阵;
  • y{0,1,,K1}Ny\in\{0,1,\ldots,K-1\}^{N}:每封邮件的类别;
  • KK:类别数,二分类垃圾邮件任务中通常为 2。

逻辑回归与 SVM 学习 P(yx)P(y\mid x) 或直接学习决策分数,属于判别式方法(Discriminative Method)。朴素贝叶斯(Naive Bayes,NB)则估计:

P(y=c),P(xy=c)P(y=c),\qquad P(x\mid y=c)

前者是类别先验(Class Prior),后者是类条件似然(Class-Conditional Likelihood)。再用贝叶斯定理(Bayes’ Theorem)反转方向:

P(y=cx)=P(xy=c)P(y=c)P(x)P(y=c\mid x)=\frac{P(x\mid y=c)P(y=c)}{P(x)}

预测同一封邮件时,分母 P(x)P(x) 对所有候选类别相同,所以分类只需比较分子:

y^=argmaxcP(y=c)P(xy=c)\hat y=\arg\max_c P(y=c)P(x\mid y=c)

这一步很重要:训练不是在背诵“优惠”必然等于垃圾邮件,而是在分别统计“类别本来多常见”和“该类别中各词多常见”。

02 “朴素”究竟假设了什么?#

若直接估计 VV 个词的联合分布 P(x1,,xVy=c)P(x_1,\ldots,x_V\mid y=c),可能的词频组合多得无法覆盖。朴素贝叶斯作出条件独立(Conditional Independence)假设:给定类别后,每个特征的出现不再依赖其他特征。

对一般特征,它把联合似然近似为:

P(xy=c)j=1VP(xjy=c)P(x\mid y=c)\approx\prod_{j=1}^{V}P(x_j\mid y=c)

文本中的“免费”和“领取”显然不是现实独立的,因此这不是对语言的忠实描述。它的价值在于把一个几乎无法估计的高维联合分布,变成每类一组可由计数估计的参数。

03 多项式模型怎样把一封邮件变成似然?#

多项式朴素贝叶斯(Multinomial Naive Bayes)把一类邮件中的每个词看成从词表分类分布中反复抽取。对类别 cc,参数向量为:

θc=[θc1,,θcV],θcj0,j=1Vθcj=1\theta_c=[\theta_{c1},\ldots,\theta_{cV}],\qquad \theta_{cj}\ge0,\quad\sum_{j=1}^{V}\theta_{cj}=1

θcj\theta_{cj} 表示类别 cc 中一次词元抽取为词 jj 的概率。若新邮件词频为 xx,总词数 M=jxjM=\sum_jx_j,其多项式似然为:

P(xy=c)=M!jxj!j=1VθcjxjP(x\mid y=c)=\frac{M!}{\prod_jx_j!}\prod_{j=1}^{V}\theta_{cj}^{x_j}

组合系数只由当前邮件 xx 决定,比较类别时可以消去。因此联合分数与下式成正比:

P(y=c,x)πcj=1VθcjxjP(y=c,x)\propto\pi_c\prod_{j=1}^{V}\theta_{cj}^{x_j}
  • πc=P(y=c)\pi_c=P(y=c):类别 cc 的先验;
  • xjx_j:新邮件中词 jj 的次数;
  • θcj\theta_{cj}:类别 cc 下词 jj 的概率。

注意指数 xjx_j 的含义:词出现两次,就把相应证据乘两次;没出现的词满足 θcj0=1\theta_{cj}^0=1,不贡献这一项。

04 从训练计数估计先验和词概率#

类别先验可用样本比例估计:

π^c=NcN\hat\pi_c=\frac{N_c}{N}

NcjN_{cj} 为训练集中类别 cc 的所有邮件里,词 jj 的总出现次数;Ncword=jNcjN_c^{word}=\sum_jN_{cj} 为该类总词数。带加性平滑(Additive Smoothing)的词概率是:

θ^cj=Ncj+αNcword+αV\hat\theta_{cj}=\frac{N_{cj}+\alpha}{N_c^{word}+\alpha V}
  • α0\alpha\ge0:平滑强度;
  • α=1\alpha=1:拉普拉斯平滑(Laplace Smoothing);
  • 0<α<10<\alpha<1:Lidstone 平滑(Lidstone Smoothing)。

若不平滑,某词从未在某类训练邮件中出现时 θcj=0\theta_{cj}=0。新邮件只要含这个词,整类似然的乘积就立刻变为 0;一个有限训练集中的“没见过”被错误解释成“绝不可能”。平滑给每个词增加伪计数,避免这类一票否决。

05 用三个词手算一封新邮件#

词表固定为 [优惠, 会议, 项目],训练集有两封垃圾邮件、两封正常邮件。汇总词频如下:

类别邮件数优惠会议项目总词数
垃圾 SS23014
正常 HH20224

两类先验相同:

πS=πH=24=12\pi_S=\pi_H=\frac{2}{4}=\frac12

α=1,V=3\alpha=1,V=3,平滑后:

θS=[47,17,27],θH=[17,37,37]\theta_S=\left[\frac47,\frac17,\frac27\right],\qquad \theta_H=\left[\frac17,\frac37,\frac37\right]

新邮件“优惠 会议”的计数向量为 x=[1,1,0]x=[1,1,0]。忽略两类共有的组合系数,联合分数为:

sS=12×47×17=249s_S=\frac12\times\frac47\times\frac17=\frac{2}{49} sH=12×17×37=398s_H=\frac12\times\frac17\times\frac37=\frac{3}{98}

因为 2/49=4/98>3/982/49=4/98>3/98,模型预测垃圾邮件。若把两个分数归一化:

P(Sx)=4/984/98+3/98=470.571P(S\mid x)=\frac{4/98}{4/98+3/98}=\frac47\approx0.571

这是一个刻意做小的例子:同一封邮件同时包含两类各自偏爱的词,因此结论并不强。它也展示了先验怎样参与竞争——若正常邮件在真实流量中远多于垃圾邮件,较大的 πH\pi_H 可能翻转结果。

交互手算:若“优惠”出现两次会怎样?

此时 x=[2,1,0]x=[2,1,0]。垃圾类与正常类的未归一化分数分别为 12(47)217=8343\frac12(\frac47)^2\frac17=\frac{8}{343}12(17)237=3686\frac12(\frac17)^2\frac37=\frac{3}{686}。两者比值从 4:34:3 扩大到 16:316:3,重复出现的词会重复贡献证据。

06 为什么工程实现必须进入对数空间?#

真实词表可能有十万维,一封邮件会连乘许多小于 1 的概率。浮点数很快下溢为 0,类别之间便无法比较。取对数把乘法改成加法:

c(x)=logπc+j=1Vxjlogθcj\ell_c(x)=\log\pi_c+\sum_{j=1}^{V}x_j\log\theta_{cj} y^=argmaxcc(x)\hat y=\arg\max_c\ell_c(x)

对数是单调函数,不改变最大值所在类别。完整数据流如下:

词表也是模型状态。推理时若重新拟合词表,同一列可能从“优惠”变成“项目”,维度即使相同,语义也已经错位。

07 不调用 fit,先写出训练本体#

下面的 NumPy 实现把计数、平滑和矩阵推理全部暴露出来:

训练的本质不是迭代梯度下降,而是一次分组计数与归一化;推理则是稀疏矩阵乘法。这解释了它为什么在超高维文本上训练和预测都很快。

08 用 scikit-learn 1.9 建立可复现流程#

截至本文写作时,scikit-learn 1.9 的 CountVectorizer 直接产生稀疏词频矩阵,MultinomialNB 接受稠密或稀疏的 [N,V] 非负特征。把两者放进 Pipeline,能保证验证与线上推理复用训练词表:

重要接口语义:

  • alpha 可为标量或 [V] 数组;alpha=1.0 是拉普拉斯平滑;
  • force_alpha=True 会保留调用者给出的极小 alpha,也意味着过小值可能造成数值问题;
  • fit_prior=True 从训练类别频率学习先验,class_prior 可显式注入已知部署先验;
  • feature_count_ 形状为 [K,V]feature_log_prob_ 与其同形;
  • predict_proba 的列顺序永远由 classes_ 决定,不能凭业务名称猜列号;
  • partial_fit 可按批增量累计计数,但第一次调用必须传入所有可能的 classes

中文文本的示例用空格表示已经分好词。真实项目不能默认把 CountVectorizer 当中文分词器:应把经过验证的 tokenizer 放进 Pipeline,并把分词词典、规范化规则与模型一起版本化。

09 训练、验证和上线时要检查什么?#

平滑强度 α\alpha、是否使用二元出现特征、n-gram 范围与最低词频都属于超参数,必须在训练折内学习词表并在验证折评价。不要先对全数据执行 fit_transform 再交叉验证,否则验证文本已经参与词表筛选。

可以用分层交叉验证同时记录概率质量与分类质量:

cv = StratifiedKFold(n_splits=2, shuffle=True, random_state=42)
scores = cross_validate(
    pipeline,
    train_texts,
    y_train,
    cv=cv,
    scoring=['neg_log_loss', 'balanced_accuracy'],
    return_train_score=True,
)
python

四条最短调试路径:

  1. 确认矩阵非负。 中心化或某些降维会产生负数,不符合多项式计数模型;
  2. 抽查列语义。 打印词表与一行非零位置,确认分词、大小写和停用词处理没有错位;
  3. 检查未登录词比例。 线上大量词不在训练词表时,它们会被忽略,输入可能接近全零;
  4. 检查分数而非只看标签。 查看 predict_joint_log_probapredict_log_proba,定位是先验压倒似然,还是某些重复词贡献过大。

10 常见错误与失败场景#

  1. 训练和推理词表不一致。 必须保存整个 Pipeline,而不是只保存分类器;
  2. 把类别字段直接整数化后交给 MultinomialNB 它适合计数或非负强度,不等于一般类别特征;一般类别特征应评估 CategoricalNB
  3. alpha=0 当成“更无偏”。 零计数会制造零概率,且 force_alpha=True 不会替你兜底;
  4. 类别失衡时只看准确率。 同时看混淆矩阵、每类召回率、PR 曲线与部署先验;
  5. 让重复模板词支配结果。 邮件签名、页脚和转发历史可能重复计算无关证据,应清洗或限制特征;
  6. 训练分布与线上分布变化。 垃圾邮件用词快速漂移,需要监控未登录词、类别比例和错误样本;
  7. 把相关性当成因果。 “优惠”提高垃圾分数,只说明训练关联,不说明该词导致邮件有害。

11 与相近方法怎样区分?#

方法输入假设训练方式主要边界
多项式 NB非负词频或强度每类累计词计数重复出现会重复贡献证据
伯努利 NB每个词出现/未出现每类累计二元事件关心是否出现,不关心次数
补集 NB用其他类别估计权重计数闭式估计常用于类别失衡文本
高斯 NB每类每维独立高斯估计均值和方差适合连续特征,不建模相关性
逻辑回归对数几率线性优化条件似然不生成词,常需更多标注数据

多项式 NB 与逻辑回归在词袋特征上都能形成线性决策分数,但参数来源不同:前者由每类词频概率之比得到,后者直接优化分类条件似然。数据很少、维度很高时 NB 往往启动快;数据增多且特征相关明显时,正则化逻辑回归常更稳。

12 今天真正需要记住什么?#

  1. 朴素贝叶斯先估计类别先验和类条件似然,再用贝叶斯定理比较后验分数;
  2. 条件独立假设把高维联合概率拆成每个特征的证据,但可能造成过度自信;
  3. 多项式 NB 用类别内词频估计 θcj\theta_{cj},拉普拉斯平滑防止未见词把整类概率归零;
  4. 推理必须在对数空间累加:X @ feature_log_prob_.T + class_log_prior_
  5. 词表、分词与分类器是同一个模型状态,必须一起验证和部署。

13 思考题与小练习#

练习 1:先验怎样翻转结论?

手算例中新邮件的类条件似然比为 P(xS):P(xH)=4:3P(x\mid S):P(x\mid H)=4:3。若部署先验变为 πS:πH=1:4\pi_S:\pi_H=1:4,联合分数比变成 1:31:3,预测会翻转为正常邮件。

练习 2:比较多项式与伯努利输入

x=[3,0,1] 二值化为 [1,0,1]。解释为什么多项式模型会把第一个词的证据计算三次,而伯努利模型只计算一次;再思考哪种更适合短标题,哪种更适合长文档。

练习 3:制造一个词表错位 bug

分别对训练文本和查询文本调用两个 CountVectorizer().fit_transform,然后打印两份 get_feature_names_out()。即使矩阵列数相同,列语义也可能不同;说明为什么只保存 MultinomialNB 不足以上线。

相关工作#

14 下一篇预告#

朴素贝叶斯为了可估计性,假设给定类别后各特征独立。若身高和体重、多个传感器读数明显相关,我们能否保留生成式分类的概率框架,同时显式建模特征协方差?下一篇将从共享高斯协方差推导线性判别分析,并解释它为什么仍然得到一条直线。

只统计词频,朴素贝叶斯怎样判断垃圾邮件?从先验到对数似然
https://zwjcode.cn/blog/naive-bayes-text-counts-log-space
作者
发布于 2026年8月25日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。