PAYATHON 2026

能否先用 topic model 筛选目标主题,再进行第二次建模?

技术老齐

可以先做一次 topic modeling,筛出相关语料,再在子集上重新建模。这种探索流程适合从宽泛主题中寻找细分主题。如果要判断某个词每次出现时的具体词义,还需要分析该位置的上下文,并验证主题与词义是否对应。

两阶段流程能否奏效,主要取决于第一轮是否保留了目标词义的实例,以及第二轮发现的差异是否确实与词义有关。

为什么主题建模不能直接等同于词义消歧?

主题模型根据词语的共现模式发现主题。例如,bank 与 loan、deposit 一起出现时,可能呈现出不同于它与 river、flood 一起出现时的主题分布。这些差异可以帮助区分“银行”和“河岸”。

不过,主题与词义没有固定的一一对应关系:

  • 同一词义可能出现在多个主题中。例如,“银行”可以出现在贷款、监管或招聘话题里。
  • 同一个主题也可能包含多个词义。一篇讨论河岸附近银行网点的文章,就可能同时使用 bank 的两种含义。

标准的词袋 LDA 忽略词序,同一个词形也使用相同的词表项。因此,即使模型发现了主题,仍需要判断词语在每个出现位置的含义。

如果已有词义清单,需要为每次出现分配词义,这属于 word sense disambiguation(WSD)。如果要发现知识库或标注数据中缺失的词义,还涉及 word sense induction(WSI,词义归纳)。无监督分组可以提供候选词义,但仍需人工检查和解释这些分组。

两阶段流程应该怎样设计?

1. 按目标词的出现位置提取上下文

如果需要逐次消歧,建议把目标词的每次出现作为一个样本,保存原文 ID、目标词位置和上下文。上下文可以只取所在句子,也可以包含相邻句子。

以整篇文档为样本适合做主题筛选,但一篇文档中可能出现多个词义,不能把文档主题直接赋给其中所有出现位置。

上下文长度需要根据语料调整:太短可能缺少线索,太长又可能引入与目标词无关的话题。

2. 第一轮建模后,检查主题对应的实际文本

主题关键词可以帮助定位候选主题。选择要保留的主题前,还要阅读对应的上下文,确认其中包含所需词义。

对于 LDA,可以根据每个样本的主题分布筛选候选文本。除了保留“概率最高的主题恰好是目标主题”的样本,也可以保留在目标主题上达到一定权重的样本。筛选阈值需要通过人工检查样本来调整,主题权重不能直接解释为词义判断正确的概率。

建议保留筛选结果和原始索引,方便回查被排除的样本。第一轮漏掉的词义实例,第二轮无法从筛选后的子集中找回。

3. 在子集上重新预处理和建模

子集的词频分布发生了变化,可以重新建立词表、调整停用词,再选择主题数。

预处理时要保留词义线索。例如,区分 bank 的含义时,删除 river、loan 这类词会减少可用信息。在确认词义之前,也不要把目标词统一替换为某个词义标签。

第二轮结果仍需检查。如果同一种词义被分成“贷款”和“存款”两组,这可能只是主题细分,不能据此认定发现了两个词义。

Python 示例:筛选后重新训练 LDA

示例使用 scikit-learn,假定 contexts 是已经准备好的字符串列表,每个元素对应目标词的一次出现及其上下文。主题数、主题 ID 和阈值仅用于演示流程,没有针对具体数据调优。

import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation


def show_topics(model, vectorizer, top_n=10):
    words = vectorizer.get_feature_names_out()
    for topic_id, weights in enumerate(model.components_):
        indices = weights.argsort()[-top_n:][::-1]
        print(topic_id, words[indices].tolist())


# 第一轮:在全部上下文上拟合
vectorizer1 = CountVectorizer(
    token_pattern=r"(?u)\b\w+\b",
    ngram_range=(1, 2),
    min_df=1,
)
X1 = vectorizer1.fit_transform(contexts)

model1 = LatentDirichletAllocation(
    n_components=5,
    learning_method="batch",
    random_state=42,
)
topic_weights1 = model1.fit_transform(X1)

show_topics(model1, vectorizer1)

# 查看每个主题权重最高的上下文,辅助人工判断
for topic_id in range(model1.n_components):
    top_rows = np.argsort(topic_weights1[:, topic_id])[-3:][::-1]
    print(f"\nTopic {topic_id}")
    for row in top_rows:
        print(int(row), contexts[row])

阅读关键词和上下文后,再选择要保留的主题。下面的 {0} 必须根据实际结果修改,主题编号本身没有固定语义。

target_topic_ids = {0}
threshold = 0.3

# 多个目标主题的权重求和,用于筛选候选上下文
target_weight = topic_weights1[:, sorted(target_topic_ids)].sum(axis=1)
selected_indices = np.flatnonzero(target_weight >= threshold)
selected_contexts = [contexts[i] for i in selected_indices]

if len(selected_contexts) < 2:
    raise ValueError("筛选后的样本不足,请检查主题选择和阈值。")

# 第二轮:在筛选后的子集上重新建立词表并训练
vectorizer2 = CountVectorizer(
    token_pattern=r"(?u)\b\w+\b",
    ngram_range=(1, 2),
    min_df=1,
)
X2 = vectorizer2.fit_transform(selected_contexts)

model2 = LatentDirichletAllocation(
    n_components=2,
    learning_method="batch",
    random_state=42,
)
topic_weights2 = model2.fit_transform(X2)

show_topics(model2, vectorizer2)

重新建模后,topic_weights2 仍然是主题分布,需要结合实际上下文判断哪些主题对应哪些词义。

LDA 的输入应为非负词频计数,因此示例使用 CountVectorizer。处理中文时,应先分词并用空格连接词语,或提供自定义 tokenizer;默认的词边界处理不能代替中文分词。

如何判断这条流程是否值得保留?

准备一小批人工标注的上下文,覆盖常见词义和少见词义,比较直接建模与两阶段建模的结果。将模型输出映射为词义判断后,再计算目标词义的 precision、recall 和 F1。

评估时尤其要检查第一轮排除的文本。只看最终保留的样本,会掩盖目标词义的漏检。主题看起来更集中,也不一定意味着消歧效果更好。

如果不同词义的局部措辞差别明显,可以尝试对目标词每次出现的上下文表示进行聚类,例如使用包含目标词位置信息的上下文向量。这类方法同样需要人工核查,因为聚类也可能按话题、句式或来源分组。

对于知识库和监督方法没有覆盖的含义,应允许标记为“未知词义”或留待审核,避免把所有实例强行归入已有词义。

备注:内容仅供参考。