Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

番外篇:LDA 与 SparseLDA

主题定义

LDA(Latent Dirichlet Allocation,潜在狄利克雷分配)是一种经典的概率主题模型,用来发现文档集合中的潜在主题结构。它假设每篇文档由多个主题混合而成,而每个主题都是词汇表上的一个概率分布。

LDA 的生成过程:

  • 主题—词语分布:对每个主题 \(t\),从狄利克雷先验中采样词语分布 \(\phi_t \sim \operatorname{Dirichlet}(\beta)\)
  • 文档分布:对每篇文档 \(m\),从狄利克雷先验中采样主题分布 \(\theta_m \sim \operatorname{Dirichlet}(\alpha)\)
  • 生成词语:对文档中的每个位置 \(n\)
    • 根据 \(\theta_m\) 采样主题 \(z_{m,n}\)
    • 根据该主题的词语分布 \(\phi_{z_{m,n}}\) 采样词语 \(w_{m,n}\)

这里的 \(\theta_m\) 和 \(\phi_t\) 都是隐变量,不是人工预设的固定分布。训练 LDA 的过程,就是根据观察到的词语反推这些隐变量。

到底主题是什么

实际上,主题就是词汇表上的概率分布。每个主题代表一个语义相关的词语集合。

示例主题:

主题 0(科技):

P(w|z=0) = {
  "手机": 0.12,  "苹果": 0.08,  "电脑": 0.10,
  "软件": 0.07,  "网络": 0.09,  "数据": 0.06,
  "算法": 0.05,  "程序": 0.04,  "系统": 0.03,
  "应用": 0.04,  "技术": 0.05,  ...
}

主题 1(体育):

P(w|z=1) = {
  "足球": 0.15,  "比赛": 0.12,  "球员": 0.10,
  "进球": 0.08,  "教练": 0.07,  "联赛": 0.09,
  "冠军": 0.06,  "球队": 0.11,  "场地": 0.04,
  "训练": 0.05,  "战术": 0.03,  ...
}

主题 2(美食):

P(w|z=2) = {
  "苹果": 0.06,  "牛肉": 0.09,  "餐厅": 0.08,
  "味道": 0.10,  "烹饪": 0.07,  "食材": 0.11,
  "美味": 0.09,  "菜谱": 0.05,  "香甜": 0.04,
  "新鲜": 0.06,  "营养": 0.05,  ...
}

举个生成的例子

假设有这么一个文档,其主题分布为:

P(z|文档) = {主题0: 0.6, 主题1: 0.1, 主题2: 0.3}

过程演示:

假设有这么一个文档:

最新的手机配备了先进的苹果芯片,其内置软件能够高效处理海量数据。
通过智能算法和网络连接,这个程序运行流畅。
今天的比赛很精彩,现场还有美味的苹果和各种食物,味道很棒。

该文档包含的关键词是:["手机", "苹果", "软件", "数据", "算法", "网络", "程序", "比赛", "美味", "味道"]

那么这些关键词的生成过程大概是这样:

  1. 第0个词:采样主题 → 主题0(科技),采样词语 → “手机”
  2. 第1个词:采样主题 → 主题0(科技),采样词语 → “苹果”(指苹果公司/芯片)
  3. 第2个词:采样主题 → 主题0(科技),采样词语 → “软件”
  4. 第3个词:采样主题 → 主题0(科技),采样词语 → “数据”
  5. 第4个词:采样主题 → 主题0(科技),采样词语 → “算法”
  6. 第5个词:采样主题 → 主题0(科技),采样词语 → “网络”
  7. 第6个词:采样主题 → 主题0(科技),采样词语 → “程序”
  8. 第7个词:采样主题 → 主题1(体育),采样词语 → “比赛”
  9. 第8个词:采样主题 → 主题2(美食),采样词语 → “美味”
  10. 第9个词:采样主题 → 主题2(美食),采样词语 → “味道”

这个例子展示的是词语及其主题的生成关系,而不是一段有顺序的文本。LDA 是词袋模型,只建模词语在文档中的共现,不建模局部词序,因此不能像自回归语言模型一样生成连贯句子。

注意到,“苹果”可以同时出现在多个主题中。LDA 会利用整篇文档中的词语共现,为“苹果”的每一次出现推断一个主题,因此能够在一定程度上反映多义性。不过,LDA 是词袋模型,不使用局部词序,也不能保证完成严格的词义消歧。

关键公式

LDA 模型可以通过 Gibbs 采样来推断每个词语的主题分配。关键的采样公式为:

$$ P(z_{m,n}=t \mid \mathbf{z}{-m,n}, \mathbf{w}) \propto \frac{(\alpha + n{t|m}^{-m,n})(\beta + n_{w|t}^{-m,n})} {\sum_v(\beta + n_{v|t}^{-m,n})} $$

该公式表示:对于文档 m 中第 n 个词语,其主题分配为 t 的概率正比于右侧的表达式。

完整的文档—主题因子还包含分母 \(n_{\cdot|m}^{-m,n}+K\alpha\)。对同一个词语枚举候选主题 \(t\) 时,这个分母保持不变,因此可以在未归一化的 Gibbs 采样权重中省略。词语—主题因子的分母则随 \(t\) 变化,不能省略。

分项解释:

  • 第一部分 \((\alpha + n_{t|m}^{-m,n})\):文档-主题倾向

    • 表示去除当前词语后,文档 m 中分配给主题 t 的词语数量
    • \(\alpha\) 是平滑参数,避免零概率
  • 第二部分 \((\beta + n_{w|t}^{-m,n})\):词语-主题倾向

    • 表示去除当前词语后,全部语料中词语 w 被分配给主题 t 的次数
    • \(\beta\) 是平滑参数
  • 分母部分 \(\sum_v(\beta + n_{v|t}^{-m,n})\):归一化项

    • 表示主题 t 的总词语数量,用于归一化

直观理解: 假设当前词语是“苹果“,候选主题是“科技“:

  • 如果当前文档中已有很多词语被分配给“科技“主题(第一部分)
  • 且“苹果“在“科技“主题中出现概率高(第二部分)
  • 那么“苹果“被分配给“科技“主题的概率就很高

SparseLDA

标准 LDA 每次采样需要遍历全部主题,时间复杂度为 \(O(K)\),其中 \(K\) 是主题数。当主题数很大时,这会成为性能瓶颈。SparseLDA 通过将采样概率分解为三个“桶”来减少实际需要遍历的主题。

数学推导

由标准的 LDA 采样公式开始:

$$ P(z_{m,n}=t) \propto \frac{(\alpha+n_{t|m}^{-m,n})(\beta+n_{w|t}^{-m,n})} {\beta V+n_{\cdot|t}^{-m,n}} $$

对记号简化,定义:

  • \(n_{t|m}\) :文档 m 中分配给主题 t 的词数
  • \(n_{w|t}\) :词语 w 被分配给主题 t 的次数
  • \(n_{·|t}\) :主题 t 的总词数

第一步:展开分子

把分子展开: $$ (\alpha+n_{t|m})(\beta+n_{w|t}) = \alpha\beta + \alpha n_{w|t} + n_{t|m}\beta + n_{t|m}n_{w|t} $$

第二步:组织成三个部分

把展开式重新组织: $$ \frac{\alpha\beta + \alpha n_{w|t} + n_{t|m}\beta + n_{t|m}n_{w|t}} {\beta V + n_{\cdot|t}} $$

$$ \frac{\alpha\beta}{\beta V + n_{\cdot|t}} {}+ \frac{n_{t|m}\beta}{\beta V + n_{\cdot|t}} {}+ \left( \frac{\alpha n_{w|t}}{\beta V + n_{\cdot|t}} {}+ \frac{n_{t|m}n_{w|t}}{\beta V + n_{\cdot|t}} \right) $$

第三步:得到三个桶

桶 s (平滑项): $$ s_t = \frac{\alpha\beta}{\beta V + n_{\cdot|t}} $$

桶 r (文档项): $$ r_t = \frac{n_{t|m}\beta}{\beta V + n_{\cdot|t}} $$

桶 q (词语项): $$ q_t = \frac{\alpha n_{w|t}}{\beta V + n_{\cdot|t}} {}+ \frac{n_{t|m}n_{w|t}}{\beta V + n_{\cdot|t}} = \frac{(\alpha + n_{t|m})n_{w|t}}{\beta V + n_{\cdot|t}} $$

平滑桶 \(s\) 仍然包含全部 \(K\) 个主题;文档桶 \(r\) 只包含当前文档中出现过的主题;词语桶 \(q\) 只包含当前词语被分配过的主题。后两个集合通常远小于 \(K\)。平滑桶的总质量可以维护起来,并且采样通常只需遍历实际落入的那个桶,因此平均开销会明显低于每次完整遍历全部主题。

具体算法

采样步骤:

  1. 计算桶的总质量: $$ S = \sum_t s_t {}+ \sum_{t:n_{t|m}>0} r_t {}+ \sum_{t:n_{w|t}>0} q_t $$

  2. 按比例选择桶:

    • 随机数生成 \(u \sim \text{Uniform}(0,S)\)
    • 若 \(u < S_s\), 选择桶 s
    • 若 \(S_s \leq u < S_s + S_r\),选择桶 r
    • 否则选择桶 q
  3. 桶内采样:

    • 根据选中的桶,对应的候选主题中概率采样

平滑桶的总质量 \(S_s=\sum_t s_t=\sum_t\frac{\alpha\beta}{\beta V+n_{\cdot|t}}\) 需要缓存,否则每个词语都重新求和仍要遍历全部主题。理论上,每次主题移动只改变旧、新两个主题的总计数,可以用 \(O(1)\) 增量精确更新;Semat 当前实现选择在每轮迭代开始时通过 UpdateCache 重新计算一次,轮内使用该缓存值,因此这里采用的是近似更新。

多线程

仅仅降低单次采样的时间复杂度还不够,还可以进一步利用多线程。观察 SparseLDA 的采样公式可以发现:合理划分文档和词汇块,能够消除大部分文档—主题计数与词语—主题计数的写冲突,但全局主题计数仍然需要单独处理。

回顾下采样公式: \(P(z_{m,n}=t) \propto \frac{(\alpha+n_{t|m})(\beta+n_{w|t})}{\beta V + n_{·|t}}\)

计数依赖分析:

  • 更新文档 m 中词语 w的主题时,会影响:
    • \(n_{t|m}\) : 文档m的主题计数
    • \(n_{w|t}\) : 词语w的主题计数
    • \(n_{·|t}\) : 主题的总计数

竞争条件问题: 若多个线程同时操作,会造成这些数据出现不一致:

  • 同一文档的不同词语:会竞争修改 \(n_{*|m}\)
  • 不同文档的相同词语:会竞争修改 \(n_{w|*}\)
  • 任意词语:都会竞争修改 \(n_{·|*}\) (这个竞争无法避免)

一种直接做法是为共享计数加锁,但锁竞争可能抵消并行带来的收益。

N-Queen 方案 类似于 N 皇后问题中皇后之间不能相互攻击的约束,让不同线程操作的文档块和词汇块互不相交,可以避免这两类局部计数之间的竞争。

核心思想:

  • 把文档和词汇表都分块
  • 不同线程负责不同的文档-词汇块组合
  • 通过巧妙的调度避免竞争

局部计数无冲突条件: 线程 i 处理文档块 \(D_i\) 和词汇块 \(V_i\),线程 j 处理文档块 \(D_j\) 和词汇块 \(V_j\)。满足以下条件时,文档—主题计数和词语—主题计数不会发生写冲突:

  • \(D_i \cap D_j = \varnothing\)(文档块不相交)
  • \(V_i \cap V_j = \varnothing\)(词汇块不相交)

这并不意味着整个算法已经完全无锁。Semat 的分块保证同一轮中的线程不会同时修改同一文档的 nm[m],也不会同时修改同一词语的 nv[w];但所有线程仍会修改共享的 nvsum[t]。当前代码没有为这些读写使用原子操作或锁,在 C++ 内存模型中属于数据竞争,而不仅是统计意义上的“陈旧计数”。若要得到定义明确的并行实现,应将 nvsum 改为原子计数,或者累计线程局部增量并在每轮调度后合并。

分块策略示例(4线程,8文档,8词汇):

第1轮:

        词0-1  词2-3  词4-5  词6-7
文档0-1  [T0]   ×     ×     ×
文档2-3   ×    [T1]   ×     ×  
文档4-5   ×     ×    [T2]   ×
文档6-7   ×     ×     ×    [T3]

第2轮(列循环右移):

        词0-1  词2-3  词4-5  词6-7
文档0-1   ×     ×     ×    [T0]
文档2-3  [T1]   ×     ×     ×
文档4-5   ×    [T2]   ×     ×
文档6-7   ×     ×    [T3]   ×

第3轮(继续右移):

        词0-1  词2-3  词4-5  词6-7
文档0-1   ×     ×    [T0]   ×
文档2-3   ×     ×     ×    [T1]
文档4-5  [T2]   ×     ×     ×
文档6-7   ×    [T3]   ×     ×

第4轮(完成覆盖):

        词0-1  词2-3  词4-5  词6-7
文档0-1   ×    [T0]   ×     ×
文档2-3   ×     ×    [T1]   ×
文档4-5   ×     ×     ×    [T2]
文档6-7  [T3]   ×     ×     ×

Perplexity

困惑度(Perplexity)用于衡量模型对未见文档的预测能力。标准评估应在留出的验证集或测试集上进行,而不是直接复用训练语料及其当前主题分配。

基本思想: LDA假设每个词语的生成分为两步:

  1. 根据文档的主题分布选择一个主题 t
  2. 根据主题 t 的词语分布选择一个词语 w

主题是隐变量,因此计算词语的边缘概率时,不能只使用当前分配到的一个主题,而要对全部主题求和。对于文档 \(m\) 中位置 \(i\) 的词语:

$$ P(w_{m,i}\mid m) = \sum_{t=1}^{K}P(t\mid m)P(w_{m,i}\mid t) $$

其中:

  • \(P(t|m) = \frac{n_{t|m}+\alpha}{n_m + K\alpha}\) : 文档 m 中主题 t 的概率
  • \(P(w|t) = \frac{n_{w|t}+\beta}{n_t + V\beta}\) : 主题 t 中词语 w 的概率

对数似然函数:

$$ \log P(\text{全部词语}) = \sum_{m=1}^{M}\sum_{i=1}^{N_m} \log\left( \sum_{t=1}^{K}P(t\mid m)P(w_{m,i}\mid t) \right) $$

困惑度(Perplexity) 定义:

$$ \text{Perplexity} = \exp\left( {}-\frac{\log P(\text{全部词语})}{\text{总词数}} \right) $$

其中总词数为测试集中所有文档的词语数量。困惑度可以直观理解为模型在每个位置面对的平均有效候选数;在相同数据集和评估方法下,数值越低,表示模型的预测能力越好。实际评估时,还需要为测试文档推断文档—主题分布,并避免使用待评估词语本身的信息。

训练过程中的近似指标

标准困惑度需要在测试文档上推断主题分布,并对全部主题求和,计算成本较高。训练过程中为了快速观察模型变化,也可以利用每个词语当前的主题分配,计算一个近似得分:

$$ \log P(z_{m,i}\mid m)+\log P(w_{m,i}\mid z_{m,i}) $$

将所有词语的得分相加,再按词数取负平均并求指数,就能得到一个随训练过程变化的指标。它利用的是训练语料及其当前主题分配,适合观察同一次训练是否趋于稳定;但它没有对隐含主题进行边缘化,也没有评估未见文档,因此不能代替前面定义的标准困惑度。

配套实现:Ismantic/Semat