ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

面向景区评论的Jieba本地词库分词优化与LDA主题分析

面向景区评论的Jieba本地词库分词优化与LDA主题分析

去年接了个景区舆情分析的需求,手头是一万六千多条游客评论,从点评站、问卷开放题和几个社媒渠道拼起来的。清洗完丢进 LDA,跑出来五个主题,三个几乎长得一模一样:景区 / 人 / 多 / 排队 / 不错。当时第一反应是调参,把主题数从 5 试到 15,alpha 从 0.1 调到 1.0,跑了七八轮,主题词还是那几张老面孔。真正的问题根本不在 LDA 那一侧,而是喂进去的"词"本身就已经碎掉了——玻璃栈道被切成玻璃和栈道,摆渡车被切成摆渡和车,检票口被切成检票和口。这些碎片在文档-词矩阵里高频出现却毫无语义承载,主题自然就糊成一团。

这篇东西讲的是怎么用一份自建的本地词库,把 Jieba 的中文分词从"通用场景够用"掰到"景区评论够准",再让下游的 LDA 真正跑出可读的主题。适合正在做中文文本挖掘、被分词结果折磨过、或者跑完 LDA 发现主题没法向业务方解释的人看。前置要求不高,会写 Python、装过 jieba 和 gensim 就能跟下来,但里面的词表构建流程和评估方法,是我踩了好几轮坑之后才理顺的,直接抄能省不少时间。

1. 景区评论这类语料,难点到底卡在哪

先把语料本身的性质说清楚,后面所有的设计都是从这儿推出来的。景区评论属于典型的"短文本 + 强领域 + 高噪声"三合一语料,它跟新闻语料、论文摘要完全是两个物种,拿通用分词器直接处理,效果落差会比想象中大得多。我见过不少人拿 THUCNews 上跑通的一套流程直接套到点评数据上,然后困惑为什么主题一致性分数低得离谱。

1.1 短文本、口语化和噪声的三重夹击

单条评论的平均长度我统计过,中位数只有 23 个字,大量样本就是"还行吧,就是人太多""风景不错,索道排队一小时"这种。短文本的致命问题在于共现信息极度稀疏——一条评论里能同时出现的实词可能就五六个,LDA 靠的是词共现来推断主题分布,样本一短,共现矩阵就大面积是零,模型只能抓住最高频的那几个通用词,主题自然塌缩。

口语化带来的麻烦同样不小。"索道排队排到怀疑人生""厕所干净得不像话""性价比一般般",这类表达里的程度词、语气词、网络化缩写占比很高,如果不清洗,一般般、不像话、怀疑人生这类词会占据大量词表位置。更麻烦的是同一件事有无数种说法:讲排队,有人写"人挤人",有人写"摩肩接踵",有人写"排队两小时",还有人直接甩一个"劝退"。这些表达在语义上高度接近,但在词表层面上完全不同,LDA 无法自动把它们归并。

噪声部分则是老生常谈但躲不开的:表情符号转成的文字、重复标点、错别字("揽车"代"缆车"、"做"代"坐")、夹杂的英文缩写、以及大量无意义的单字。我在清洗阶段做过一次统计,原始语料里单字 token 占比接近 31%,这些单字进入词表后基本只贡献噪声。

提示:短文本语料不建议直接上 LDA,先把长度低于 8 个字的评论单独拿出来看一眼,很多时候它们的价值在于情感极性而不是主题归属,混进去反而拖累主题质量。

1.2 专有名词才是真正的分词黑洞

如果说短文本和口语化是"普遍性困难",那专有名词就是景区这个垂直领域特有的深坑。我大致把踩过的坑分成三类,你看完就明白为什么通用词典在这里几乎失效:

类型典型例子通用分词器的切法造成的后果
景点/设施名玻璃栈道、观光车、游客中心、母婴室玻璃/栈道、观光/车、游客/中心、母婴/室语义单元被打散,主题词表里全是后半截
组合式评价短语性价比高、体验感差、拍照出片、遛娃友好性/价比/高、体验/感/差评价维度无法聚合成类
方言与本地表达巴适、闹热、耍起、蛮好逐字切分或误合成HMM 兜底容易切出奇怪的双字组合

第一类最要命。景点名和设施名恰恰是区分主题的核心线索——讲索道体验的、讲餐饮的、讲住宿的、讲停车配套的,本来就该是不同的主题簇。一旦索道被切成索和道,道这个字在语料里到处都是(步道、车道、味道、难道),模型根本没法把它和索道体验关联起来。我在第一版结果里就看到过主题词是道 / 走 / 累 / 山 / 上去,完全没法向业务方交代。

第二类属于评价维度词,它们在评论里出现频率极高,是主题聚类的天然锚点。性价比被切成性、价比之后,性价比高和性价比低这两类完全对立的评价反而在词表里合并成了噪声,这就不是精度问题了,是方向性错误。

第三类的影响面小一些,但在做地域性景区分析时会明显暴露。HMM 的未登录词识别是靠字序规律猜的,它不认识"耍起"这种方言搭配,经常两个字合成一个不存在的词,或者干脆拆成两个单字。

2. Jieba 切词到底按什么逻辑走

想改得准,先得知道它在干什么。很多人对 Jieba 的印象停留在"加个自定义词典就行",但词频怎么生效、HMM 什么时候介入、词性标注是独立的还是绑定的,这些细节直接决定了你的词库该怎么写。

2.1 前缀词典、DAG 与动态规划的主干路径

Jieba 的核心流程分三步走,我用一个具体例子过一遍。假设输入是"玻璃栈道太刺激了":

第一步,构建前缀词典。词典里所有的词连同它们的词频一起,被组织成前缀树结构,这样查"玻璃栈道"的时候,能快速判断"玻"、"玻璃"、"玻璃栈"、"玻璃栈道"哪些是词。这一步的输入就是dict.txt加上你后续加载的用户词典。

第二步,生成 DAG(有向无环图)。对句子里的每个起始位置,找出从这儿出发所有可能的成词终点。对"玻璃栈道",可能得到玻、玻璃、玻璃栈、玻璃栈道等多条路径,每条边带着对应词频。

第三步,动态规划求最大概率路径。它用的是log(freq) - log(total),也就是把词频转成对数概率后相加,取总和最大的那条切分。这一步才是真正的决策点,词频数据直接决定了哪条路径胜出。假设通用词典里"玻璃"的词频是 8000,"栈道"是 1200,而"玻璃栈道"这个词压根不存在,那么玻璃/栈道这条路径的总概率必然高过玻/璃/栈/道,结果就固定死了。

理解这一点非常关键:你要让一个领域词被正确切出来,光把它加进词典还不够,还得让它的词频高到足以在概率竞争中胜出。这是后面调词频的理论依据。

2.2 HMM 兜底与未登录词的边界

DAG 走完之后,句子会被切成一堆词和单字的混合序列。所有连续的单字会被重新拼起来,丢给 HMM 做二次切分。这就是 Jieba 的"未登录词识别"机制,用的是 BMES 四个状态的隐马尔可夫模型,靠字与字之间的转移概率判断该不该成词。

这个机制是一把双刃剑。好处是能识别出"摆渡车""检票口"这类词典里没有但字面上常见的组合;坏处是它会误伤已经被正确切开的单字。我碰过一个特别典型的案例:厕和所在某些上下文里被 HMM 判定为两个独立语素,结果厕所这个词在部分评论里消失了。原因是这几个字在训练语料里的转移概率不站在这一侧。

所以结论很明确:只要是你确定要保留的领域词,最好显式加进词典,不要指望 HMM 兜底。HMM 是用来处理长尾未知词的,它的判断标准是统计规律,不是领域知识,指望它理解"母婴室"是一个完整设施,不太现实。

注意:Jieba 还有个HMM=True/False的总开关。关掉 HMM 会让切分完全由词典决定,结果是极端的确定性——好的一面是不会有意外合并,坏的一面是所有未登录词都会碎成单字。我的做法是保持 HMM 开启,同时把核心领域词全部显式收进词表,两边都占。

2.3 词频、词性与 suggest_freq 的真实作用

词频的默认值,Jieba 是按词频 = int(1e8 / 词长)自动估的,也就是说你加一个三字词,它给个约 3300 万的词频,比通用词典里的常用词还高。这个默认值在多数场景下是好事,但也可能引发过度切分,比如你把"停车场入口"这样一整个短语加进去,它就可能把"入口"这个本身有独立意义的词从其他句子里一并吞掉。

suggest_freq是个更精细的工具,它可以在不修改词典的前提下,动态把某个词的词频调到刚好能切开的位置。我有两个典型用法:

  • 强制切开:某个短语在特定语境下需要拆开,比如"不要/走路"和"不要走/路",用jieba.suggest_freq(('不要', '走'), True)调整。
  • 强制合并:某个词共现极高但你不想动词典,用jieba.suggest_freq('玻璃栈道', True)在运行时提权。

词性方面,jieba.posseg走的是另一套标注(基于 ICTCLAS 的标注集),它和分词是并行的,不是先切后标。给领域词加词性是有实际价值的:你可以只保留n(名词)、nr(人名)、ns(地名)、vn(动名词)这类有实义的词性,把d(副词)、p(介词)、u(助词)批量过滤掉,比维护一份庞大的停用词表更省事。

3. 本地词库怎么攒:从原始评论里"捞"词

词典是整件事的地基,地基歪了后面全是白费。我的做法是不靠拍脑袋列词,而是从语料里用统计方法捞候选,再走人工复核,两个阶段分工明确。

3.1 领域词的三类来源与优先级

我一般从三个地方收集:

第一来源是业务侧提供的名词表。景区官网、导览图、票务页面上的设施名、景点名、服务项目名,这是最干净也最权威的一批。这部分词优先级最高,直接全量入库。这类数据往往就在一些不起眼的角落里,比如导览页面的锚点文字、票种说明里的短语,抓一次能用很久。

第二来源是高频 n-gram 抽取。把全部评论做 2-gram 到 5-gram 的频次统计,取出现次数超过阈值的组合。这里会出现大量有用的领域词,也会混进一堆"很好很不错""不太推荐""非常满意"这种通用短语。判别方法是看这个词是否有领域指向性:游客中心有,非常好没有。

第三来源是新词发现算法的输出。前两个来源覆盖不到的,就得靠统计方法挖。下一节展开讲。

三者的优先级我是这样排的:业务名词表 > 高频 n-gram(人工筛后) > 新词发现(人工筛后)。越靠后噪声越大,人工复核成本越高。

3.2 用互信息加邻接熵挖新词

新词发现这套方法的核心思想很朴素:一个真正的词,内部字与字应该"粘得紧",外部搭配应该"散得开"。前者用互信息衡量,后者用左右邻接熵衡量,两个指标都过关的候选才算数。

互信息的算法是PMI(x,y) = log2( P(xy) / (P(x) * P(y)) )。直觉上理解,如果"栈"和"道"只是碰巧挨在一起,它们共现的概率应该接近各自出现概率的乘积,PMI 接近 0;如果它们真的构成一个词,共现概率会显著高于随机期望,PMI 就会明显为正。阈值一般取 1.0 到 2.0 之间起步,具体数字要看你语料规模,我是用了一个从 0.8 到 2.5 的扫描,人工看每个阈值下的输出质量再定的。

邻接熵是另一侧,它衡量的是候选词的左右两侧能接多少种不同的字。计算公式是H = -Σ p(c) * log2 p(c),其中 p(c) 是每个邻接字出现的概率。熵值越高说明这个词越独立,能自由应用在各种上下文里;熵值低说明它总是固定出现在某个大短语里,那它可能只是更大短语的一部分。

给个简化实现,能直接跑:

from collections import defaultdict import math def extract_candidates(texts, min_count=20, pmi_thr=1.5, entropy_thr=1.2, max_len=5): # 统计单字、n-gram、左右邻接 word_cnt = defaultdict(int) left_chars = defaultdict(lambda: defaultdict(int)) right_chars = defaultdict(lambda: defaultdict(int)) total_words = 0 for text in texts: chars = list(text) n = len(chars) for i in range(n): for L in range(2, max_len + 1): if i + L > n: break w = ''.join(chars[i:i + L]) word_cnt[w] += 1 total_words += 1 if i > 0: left_chars[w][chars[i - 1]] += 1 if i + L < n: right_chars[w][chars[i + L]] += 1 def entropy(counter): total = sum(counter.values()) if total == 0: return 0.0 return -sum((c / total) * math.log2(c / total) for c in counter.values()) results = [] for w, cnt in word_cnt.items(): if cnt < min_count or len(w) < 2: continue # 计算最小 PMI(字切成两半,取最低的那一刀) pmi_min = float('inf') for cut in range(1, len(w)): a, b = w[:cut], w[cut:] pa = word_cnt.get(a, 0) / total_words pb = word_cnt.get(b, 0) / total_words pw = cnt / total_words if pa == 0 or pb == 0: pmi_min = -1 break pmi_min = min(pmi_min, math.log2(pw / (pa * pb))) h = min(entropy(left_chars[w]), entropy(right_chars[w])) if pmi_min >= pmi_thr and h >= entropy_thr: results.append((w, cnt, round(pmi_min, 2), round(h, 2))) return sorted(results, key=lambda x: -x[1])

跑完之后按频次倒序看前 300 条,人工过一遍,能留下的通常有几十个高质量领域词。这一步不要偷懒交给模型自动筛,我试过用规则直接过滤,结果漏掉了好几个低频率但极关键的词,比如"无障碍通道",出现不到 30 次,但它是"配套设施"这个主题的核心线索。

3.3 词典文件的格式与人工复核清单

Jieba 的用户词典格式很朴素:词语 词频 词性,用空格分隔,词频和词性都可以省略。我一般写全三项:

玻璃栈道 800 n 观光车 650 n 游客中心 600 n 母婴室 200 n 排队时长 300 n 性价比 900 n 出片 400 a

人工复核的时候我会对照一份自检清单,这几条是我筛了几千个候选词之后沉淀下来的经验:

  • 是否有独立语义:像"各种各"这种明显是跨词边界截断的,直接删。
  • 是否会被更长的词覆盖:游客中心已经在库里,再加一个游客就没意义了,反而可能干扰。
  • 是否会引起误切:加"很不"这种高频组合词,会把"很不错"从短语里拆出来,得不偿失。
  • 词频是否合理:名词类给 200 到 900 之间比较稳,太高的会压制其他词的正常切分,太低的话在概率竞争里打不过通用词,加了等于没加。

提示:词频不要一次性写在最终值,先用默认值跑一遍,看看哪些词还是被切碎了,再针对性地往上调。我一般从 200 起步,每轮加 200 测试,两三轮就能收敛。

4. 把词库接进 Jieba:加载顺序与实测对比

词表攒好了,接下来是工程接入。这块看着简单,实际上有几个细节不注意的话,词库加载了但不生效,排查起来很费时间。

4.1 load_userdict 与 add_word 的差异

两个方法的功能都是加词,但适用场景完全不同:

方式是否修改全局词典词频处理适用场景
jieba.load_userdict(path)是,一次载入按文件里写的词频,无则自动估工程化、词表稳定、批量维护
jieba.add_word(word, freq, tag)是,逐个添加显式指定,可动态调整临时调试、少量动态补充
suggest_freq(name, tune)是自动调优到能切开的临界值精准控制某个词的合并/拆分

第一个坑是加载顺序。如果代码里先调用了jieba.cut触发了惰性初始化(Jieba 首次使用时会加载主词典),之后再load_userdict,虽然词是加进去了,但在此之前已经切过的文本不会重算。所以务必要在模块初始化阶段、任何分词调用之前完成加载。

第二个坑是并行分词模式。jieba.enable_parallel()会把语料切片分给多进程,主进程加载的用户词库在子进程里不一定同步。我一开始就栽在这儿——单进程测试明明切得对,开了并行之后结果不一致,调了半天才发现是这个问题。要么不开并行,要么在子进程的初始化函数里再加载一次。

第三个坑是词性标注的缓存。posseg是有内部缓存的,如果你在同一个进程里先跑了不带用户词典的标注,后续加了词再跑,缓存命中会导致结果不变。稳妥的做法是每次实验都重启进程,别在同一个 kernel 里反复起停。

4.2 一条能复用的预处理流水线

下面这段是我在实际项目里用了挺久的一个版本,从原始文本到最终分词结果,中间几个环节的顺序是有讲究的:

import re import jieba import jieba.posseg as pseg DOMAIN_DICT = "dict/domain_words.txt" STOPWORDS_PATH = "dict/stopwords.txt" KEEP_POS = {'n', 'nr', 'ns', 'nt', 'nz', 'vn', 'v', 'a', 'an'} def init_tokenizer(): # 必须在任何切分动作之前完成 jieba.load_userdict(DOMAIN_DICT) # 对几条最容易误切的短语做精准干预 jieba.suggest_freq(('不', '推荐'), True) jieba.suggest_freq('游客中心', True) return {w.strip() for w in open(STOPWORDS_PATH, encoding='utf-8') if w.strip()} def clean(text): text = re.sub(r'http\S+|www\.\S+', '', text) # 去掉链接 text = re.sub(r'\[.*?\]', '', text) # 去掉表情文本标记 text = re.sub(r'(\d)\1{2,}', r'\1', text) # 压缩重复数字 text = re.sub(r'([!?。,])\1+', r'\1', text) # 压缩重复标点 text = re.sub(r'[a-zA-Z]{1,2}', '', text) # 去掉短英文噪声 return text.strip() def tokenize_batch(texts, stopwords): out = [] for t in texts: t = clean(t) if len(t) < 6: continue words = [] for w, flag in pseg.cut(t): w = w.strip() if len(w) < 2: continue if w in stopwords: continue if flag in KEEP_POS: words.append(w) if len(words) >= 3: # 过短的文档对 LDA 无贡献 out.append(words) return out

几个设计点值得单独说一下。过滤长度小于 2 的词,是因为单字在中文里歧义太严重,山、水、道这类字进入词表只会拉低主题可读性,代价是损失了少量信息,但收益明显更大。过滤词性用的是白名单策略而不是黑名单,因为黑名单永远列不全。要求文档至少保留 3 个词,这是为了保证进入 LDA 的每篇文档都有基本的共现结构,低于这个数的文档对主题分布的贡献接近随机噪声。

4.3 分词效果怎么验证,不能只靠眼睛

"看着顺眼"不是评估标准。我一般做两件事:

一是抽样人工核对。随机抽 100 条评论,把优化前后的分词结果并排打印出来,逐条看关键领域词有没有被正确切出。这个动作成本不高但极有效,我在第二轮就靠它发现"检票口"在某种句式下仍然被切开的问题。

二是看词表层面的指标。统计几个数字:领域词在分词结果中的完整保留率(词表里的词在语料中实际被完整切出的比例)、平均词长(优化后应该明显上升)、单字 token 占比(应该显著下降)。我这次的数据是:完整保留率从 61% 提到 94%,平均词长从 1.42 涨到 1.89,单字占比从 31% 降到 12%。这三个数字比任何主观判断都可靠。

5. 分词改完之后,LDA 那边要跟着调的事

这是最容易被忽略的一环。很多人以为分词优化完就万事大吉,直接把新词表扔进原来的建模脚本,结果分数没涨多少,还以为是分词没用。词表变了,文档-词矩阵的形状就变了,LDA 的超参和过滤阈值必须跟着重调。

5.1 词表膨胀带来的稀疏度变化

分词优化最直接的结果是词表规模下降但有效词增多。原来"玻璃"、"栈道"是两个词,现在合并成一个"玻璃栈道",词表里少了一个,但这个词的语义完整度反而提升了。我这次词表从 1.8 万降到 1.1 万,看起来是降了,但有效名词的覆盖率反而上升了。

对应的处理是filter_extremes的重设。原来我用no_below=5, no_above=0.5,优化后因为短文档被筛掉了一批,实际文档数减少,no_below需要相应下调,否则会误删大量领域词。经验做法是按文档总数的千分之一到千分之二来定no_below,一万篇文档就取 10 到 20 之间。no_above我一般放在 0.3 到 0.6 之间,太严会把"景区""游客"这类必要的高频词删掉,太松又会让它们主导所有主题。

这里有个反直觉的点值得强调:no_above设得太高(比如 0.9)会让 LDA 的主题严重同质化。因为"景区"这个词在几乎每篇文档里都出现,模型会发现把它放进任意主题都不会降低似然,于是所有主题都带上了它。我一般会显式地把这类"背景词"单独列出来,在no_above之外再手动剥离一次。

5.2 主题数与 alpha/beta 的重新拿捏

分词质量提升之后,主题数是可以适当增加的。原因很直接:原来因为词碎片化而被合并在一起的语义簇,现在能分开了。我这次从 5 个主题试到 9 个,用 c_v 一致性打分扫描:

主题数优化前 c_v优化后 c_v备注
40.3820.447主题过少,语义被强行合并
60.4110.508优化后出现明显的体验/配套分野
80.3980.531优化后最优,主题边界清晰
100.3710.492开始出现重复主题
120.3520.461明显过拟合

注意优化前的最优点在 6,优化后在 8,这个位移本身就是分词质量提升的证据——原来模型区分不出那么多语义簇,现在能了。如果你优化完分词发现最优主题数没变,那大概率是词表改得不够到位。

alpha 和 beta 这块,我用的是alpha='auto'让 gensim 自己学,beta 保持默认的 1/K 或者 0.01。短文本语料建议 alpha 往小了调,因为每篇文档涉及的主题本来就少,稀疏的先验更贴合实际。beta 太小会导致主题词表过度集中在少数几个词上,1/K 是个相对稳的起点。

5.3 主题词可读性的前后对比

光看分数不够,最终还得看主题词能不能讲成人话。我挑了一组对比放这儿,左边是优化前,右边是优化后,你能直观感受到差别在哪:

主题编号优化前的主题词优化后的主题词
T1人 多 排队 时间 小时 感觉排队时长 索道 高峰 建议 提前
T2玻 璃 栈 道 刺 激 走玻璃栈道 刺激 恐高 拍照 出片
T3车 停 场 找 半天停车场 车位 收费 接驳车 引导
T4吃 饭 贵 味道 一般餐饮 价格 味道 性价比 分量
T5厕 所 干 净卫生间 母婴室 干净 无异味 无障碍

优化前的 T1 到 T5,基本没法用;优化后每个主题都有明确指向,"索道体验""网红打卡""停车配套""餐饮价格""卫生设施"这五个维度能直接对应到景区的管理条线。这就是分词优化真正的价值——不是提高了多少分,而是让模型输出变得可交付。

顺便说个细节,优化后的主题词里出现了恐高和出片这种词,在通用词典里根本不存在,全靠本地词库补进去的。这类词恰恰是区分细分人群的关键,"恐高"的人在意的是安全感和路线提示,"出片"的人在意的是打卡点布局,两类人的诉求完全不同。没有定制词库,LDA 永远发现不了这种细分。

6. 踩过的坑和上线前的自检

这节讲几个我确实栽过的坑,都是那种"事后看很明显、当时就是想不通"的类型。

6.1 词表不是越长越好

最开始我的思路是"能想到的都加上",词表搞到 3000 多条,结果跑出来效果反而变差。排查后发现两个原因:一是短词被长词吞并,我加了"游客服务中心",结果"游客中心"这个更通用的说法在文本里也被优先匹配成长词,反而降低了覆盖;二是低频词稀释了概率分布,3000 个词里有一大半在全语料中出现不到 5 次,它们的存在只是增加了 DAG 的分支数,让动态规划的选择空间变大,反而更容易选错路径。

后来我把词表压到 800 条左右,只保留真正高频且语义独立的词,效果立刻回升。这个经验我后来反复验证过:词表规模和质量的关系不是线性的,超过某个临界点之后,每加一个词带来的边际收益都是负的。

判断标准我总结成一句话:这个词如果在语料里出现不到 30 次,加它的意义就不大,除非它在业务上极端重要(比如"无障碍通道"这种)。对于这种"低频但关键"的词,正确的处理方式不是加进主词库,而是单独做一份小词表,用add_word显式指定一个对得起它地位的词频。

6.2 停用词和领域词的冲突

常规停用词表里都有"不""没""无"这类否定词,直接过滤会出大事。推荐和不推荐在情感上是完全相反的两极,把"不"当停用词过滤掉之后,这两类评论在 LDA 里就变得无法区分了。我在第一版结果里就看到过一个主题,主题词是推荐 / 值得 / 建议,点进去看原文,一半是好评一半是差评,主题直接失效。

处理方式有两种,我倾向于第一种:把否定词和相邻词组合成整体加进词库,不推荐、没意思、不值得、不会再来各成一条,这样切分时它们就是不可分割的整体,不会被停用词表误伤。第二种是保留否定词、但从 LDA 词表里剔除,缺点是它们仍然占据文档长度,影响主题分布的归一化。

6.3 交付前的一份自检清单

跑完整个流程之后,我会对着这几条过一遍,基本能保证不留明显的问题:

  • 领域词完整率抽检:随机 100 条评论,人工数一遍关键领域词被完整切出的比例,低于 90% 就回去补词表。
  • 单字占比:超过 15% 说明清洗不够,或者词频设置有问题。
  • 文档长度分布:过滤后的文档平均长度掉到 5 个词以下,说明过滤太狠,LDA 学不到东西。
  • 主题词有无明显重复:两个主题的前十个词重叠超过一半,说明主题数偏多或者 alpha 太大。
  • 有无"背景词污染":景区、游客、感觉这类词如果出现在超过 60% 的主题里,需要单独剥离。
  • 否定词是否生效:抽查差评样本,看它们的主题归属是否明显偏离好评。

最后再分享一个小技巧。做完一轮优化之后,别急着删掉旧的分词脚本,保留一份基线,后面每次调参都拿它做对照。我在第三轮的时候一度觉得是不是改坏了,因为一致性分数掉了 0.02,回头跟基线一比才发现是主题数选错了,分词那边其实是进步的。有个稳定的对照物在手,很多判断会变得清晰得多,也省掉大量来回试错的时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进