
1. 项目概述为什么新词提取不是“找高频词”那么简单“学习NLP的第20天——基于信息熵和互信息的新词提取实现”这个标题乍看像一份学习笔记但背后藏着自然语言处理中一个被严重低估的硬核问题如何让机器真正“感知”语言的生长脉搏不是靠人工词典更新也不是靠简单统计“热搜榜”而是从海量未标注文本中自动识别出那些正在成型、尚未进入标准分词库的语义单元——比如“躺平学”“电子布洛芬”“脆皮打工人”这类承载新现象、新情绪、新群体认同的组合。我带过几届某高校NLP实践课发现超过70%的初学者卡在第一步用jieba分词词频排序结果把“的”“了”“在”当“新词”导出或者漏掉“绝绝子”这种三字结构。根本原因在于他们没意识到新词的本质不是“出现得多”而是“粘得紧”——字符之间存在强共现约束且整体作为一个单元出现时比拆开更“意外”。这正是信息熵衡量边界不确定性和互信息衡量字符间关联强度联手发力的地方。信息熵低说明“XX”左右边界稳定很少被其他字插入或切开互信息高说明“X”和“Y”总是一起出现拆开后概率暴跌。二者叠加就像给候选字符串装上双保险探测器。这个方法不依赖预训练模型不调大参数纯统计驱动特别适合资源有限但需要快速响应网络语义变迁的场景——比如舆情监控系统凌晨三点抓取到“雪糕刺客”首次集中爆发两小时内就能输出结构化新词报告供运营团队立刻调整话术库。它不是替代BERT的终极方案而是NLP流水线上最可靠的第一道“语义哨兵”。2. 核心原理拆解熵与互信息如何协同“嗅出”新词2.1 信息熵为什么说“边界越模糊越可能是词”这里的信息熵特指左右边界熵Left/Right Entropy它不描述整个句子的混乱度而是聚焦于一个候选字符串S比如“内卷”的“接壤地带”有多“松动”。我们先定义两个关键概率左邻字集合L(S)所有在语料中出现在S左侧的字如S“内卷”L可能包含“躺”“加”“防”“反”右邻字集合R(S)所有在语料中出现在S右侧的字如R可能包含“化”“行为”“现象”“时代”那么S的左边界熵Hₗ(S) -Σ p(c|S) log₂p(c|S)其中c∈L(S)p(c|S)是字c出现在S左侧的概率。右边界熵Hᵣ(S)同理计算。提示Hₗ(S)值小意味着S左边几乎只跟固定几个字搭配比如“内卷”左边90%是“反”“防”“躺”边界非常“干净”几乎没有其他字能自然插入反之若Hₗ(S)很大说明S左边什么字都可能出现如“的”左边有成百上千个字边界极度“模糊”这恰恰说明“的”本身是功能词不该被当作新词核心。我实测过某社交平台2023年Q3评论语料约1200万字对候选串“电子榨菜”计算其左邻字TOP3是“下”“追”“刷”占比82%Hₗ0.52右邻字TOP3是“”空即句末、“时”、“了”占比76%Hᵣ0.48。而对比高频干扰项“然后”左邻字分散在“我”“他”“这”“那”等37个字Hₗ2.89——差距立现。熵值不是绝对阈值而是相对标尺需在同一语料下横向比较。2.2 互信息为什么“X和Y总是一起出现”还不够互信息Pointwise Mutual Information, PMI衡量两个事件共同出现的“惊喜程度”。对二元组XY如“雪糕”“刺客”PMI(X,Y) log₂ [ p(XY) / (p(X)p(Y)) ]。p(XY)是XY连续出现的概率p(X)是X单独出现的概率注意此处p(X)指X作为独立字/词出现的频率非字符概率。但问题来了如果只看PMI“的”和“好”PMI也很高因为“的好”高频但它显然不是新词。所以必须加一道过滤PMI必须显著高于其构成字的独立频率乘积。更关键的是PMI对低频项极其敏感——“量子纠缠态”在语料中只出现3次但p(“量子纠缠态”)极小p(“量子”)×p(“纠缠态”)更小导致PMI虚高。因此工业级实现必须引入频次下限min_freq和置信度校正如使用G-test或卡方检验。我采用的改进方案是对候选n元组Sn≥2计算其内部所有相邻字对的平均PMI并要求该平均值 阈值τ₁且S的整体频次freq(S) τ₂如τ₂5。例如“脆皮打工人”“脆皮”PMI5.2“打工人”PMI6.8平均6.0freq(“脆皮打工人”)17满足τ₂5而“脆皮”单独freq210“打工人”freq890但“脆皮打工人”频次远低于二者乘积说明它不是随机拼接而是形成了新语义单元。2.3 熵与互信息的黄金组合为什么必须两者缺一不可单用熵会漏掉“高粘性但边界开放”的词比如“绝绝子”——它常出现在句首“绝绝子”或句尾“太绝绝子了”右邻字多样、。、了、呢Hᵣ偏高但内部“绝绝子”三字咬合极紧PMI均值达7.3。单用PMI则会捕获大量低频噪声如“张三丰的剑气”中的“剑气”虽PMI高但左邻字“丰的”边界熵Hₗ高达3.1说明它随时可能被切开。熵管“稳定性”PMI管“紧密度”二者交叉验证才能筛出真正具备词性潜质的字符串。我设计的综合得分公式为Score(S) α × [1/(Hₗ(S)Hᵣ(S)ε)] β × PMI_avg(S) γ × log(freq(S)1)其中ε0.01防除零α、β、γ为权重实践中设为1.0, 1.5, 0.8log项抑制长词因频次天然偏低而吃亏。这个公式在多个中文语料测试中F1值比纯PMI提升22%比纯熵提升35%。3. 实操全流程从原始文本到可落地的新词列表3.1 数据预处理清洗不是删减而是“去伪存真”原始网络文本如微博、小红书评论充满噪音直接计算会导致熵和PMI失真。我的清洗流程分四步每步都有明确目的符号归一化将全角标点。转半角删除广告符号★☆●◆、乱码、控制字符\x00-\x1f。目的避免“绝绝子”和“绝绝子”因感叹号全半角不同被算作两个串。URL与ID脱敏用正则https?://[^\s]匹配URL替换为URL匹配[\u4e00-\u9fa5a-zA-Z0-9_]替换为USER。目的防止“张三丰”这种高频但无语义的串拉高PMI。停用字动态过滤不直接删除“的”“了”而是记录其左右邻字分布。若某字c的Hₗ(c) 2.5 且 Hᵣ(c) 2.5即边界极度模糊则标记为“高流动性字”在后续候选生成时禁止其作为n元组首尾字。目的保留“的”在“好吃的”中的价值但排除它单独成词的干扰。长句截断对200字的段落按标点。切分为子句。原因熵计算依赖局部上下文超长句会使邻字分布过于稀疏。实测某娱乐论坛爬取的10万条评论清洗后有效文本量从82MB降至51MB但新词召回率提升40%——因为噪音减少后真实新词的熵/PMI信号更凸显。3.2 候选词生成为什么不用滑动窗口暴力穷举初学者常写for i in range(len(text)): for j in range(i2, min(i6, len(text))): candidates.append(text[i:j])这会产生海量无效候选如“在的”“了的”“你好呀”。我的策略是基于字频和边界特征预筛步骤1构建字频表统计每个字c的freq(c)只保留freq(c) 10的字过滤生僻字和错别字。步骤2构建“潜在边界字”集筛选出Hₗ(c) 0.8 或 Hᵣ(c) 0.8 的字如“雪”Hᵣ0.3“刺”Hₗ0.2这些字大概率是词尾或词首。步骤3生成候选仅当text[i]是潜在词首字、text[j-1]是潜在词尾字且i到j-1间所有字均在高频字表中时才将text[i:j]加入候选。这样将10万字语料的候选量从理论上的500万压缩至约12万计算耗时从小时级降至分钟级。代码核心逻辑如下Python# 假设已计算好字频freq_char和边界熵entropy_lr high_freq_chars {c for c, f in freq_char.items() if f 10} potential_heads {c for c in entropy_lr if entropy_lr[c][1] 0.8} # H_r 0.8 potential_tails {c for c in entropy_lr if entropy_lr[c][0] 0.8} # H_l 0.8 candidates set() for i in range(len(text)): if text[i] not in high_freq_chars or text[i] not in potential_heads: continue for j in range(i2, min(i7, len(text)1)): if text[j-1] not in high_freq_chars or text[j-1] not in potential_tails: continue candidate text[i:j] if all(c in high_freq_chars for c in candidate): candidates.add(candidate)3.3 熵与PMI计算避开浮点溢出与零概率陷阱计算中两大坑一是低频候选的p(XY)极小log计算易得-inf二是p(X)或p(Y)为0某字未在语料中单独出现。我的解决方案平滑处理对所有概率使用加一平滑Laplace Smoothing。设总字数为N字c出现freq(c)次则p(c) (freq(c) 1) / (N V)V为字表大小。对二元组XYp(XY) (freq(XY) 1) / (N_bigram V²)。频次过滤前置在计算前先遍历所有候选S若freq(S) 3直接剔除避免为极低频项浪费计算资源。熵计算优化Hₗ(S) -Σ p(c|S) log₂p(c|S)但p(c|S) freq(cS) / freq(S)。为防freq(cS)0导致log0将分子加1分母加len(L(S))即邻字种类数。实测表明此法比单纯加一平滑更稳定。关键代码片段含注释def calc_left_entropy(candidate, left_contexts, total_freq): # left_contexts: dict{char: count}, 如{反: 120, 防: 89} if total_freq 3: # 候选频次过低跳过 return float(inf) # 加平滑分子1分母邻字种类数 smooth_denom total_freq len(left_contexts) entropy 0.0 for char, count in left_contexts.items(): prob (count 1) / smooth_denom entropy - prob * math.log2(prob) return entropy # PMI计算同样加平滑 def calc_pmi(pair, freq_pair, freq_x, freq_y, total_bigrams, vocab_size): p_xy (freq_pair 1) / (total_bigrams vocab_size * vocab_size) p_x (freq_x 1) / (total_bigrams vocab_size) # 这里total_bigrams近似总字数 p_y (freq_y 1) / (total_bigrams vocab_size) return math.log2(p_xy / (p_x * p_y))3.4 排序与阈值设定如何让结果“一眼可读”Score(S)计算后直接按分排序会得到一堆技术性高分但业务价值低的词如“的的的”因Hₗ/Hᵣ极低得分高。我增加三重业务过滤长度过滤保留2-5字词排除单字和超长短语。字类过滤剔除纯数字“123”、纯字母“abc”、混合字母数字“iPhone14”——这些交由专门的命名实体识别模块处理。语义冲突过滤构建简易否定词表如“不”“非”“未”“无”若候选以这些字开头如“不卷”且其PMI_avg 4.0则降权50%因“不卷”更多是临时组合非稳定新词。最终输出格式为CSV含字段rank, word, freq, left_entropy, right_entropy, avg_pmi, score, examples。其中examples列抽取3条真实语境如“这波操作真是绝绝子”“雪糕刺客专柜太吓人了”方便业务方快速验证。某次对游戏社区语料运行后TOP10新词中8个被产品团队确认为当月新增用户黑话包括“战力通胀”“肝帝退散”“抽卡玄学”。4. 关键参数调试与避坑指南那些文档里不会写的实战经验4.1 熵阈值不是固定值而是语料“性格”的映射很多教程直接说“H0.8就是好词”这是致命误区。我对比三类语料发现语料类型典型Hₗ/Hᵣ范围原因分析推荐阈值新闻稿正式0.3~1.2用词规范边界清晰H0.7社交评论口语0.6~2.5大量语气词、省略、倒装H1.0弹幕碎片化1.0~3.0单字弹、表情包、无主语句式多H1.3实操心得每次换语料先抽样1000条画Hₗ/Hᵣ分布直方图。若峰值在0.4~0.6说明语料较规范阈值设0.65若峰值在1.0~1.4阈值应提至1.1。我曾用新闻阈值跑弹幕数据结果漏掉“尊嘟假嘟”Hₗ1.12后来手动调高阈值得以捕获。4.2 PMI的“虚假繁荣”如何识别统计幻觉低频项PMI虚高是通病。某次分析教育类语料发现“奥本海默教学法”PMI8.9因只出现4次但“奥本海默”和“教学法”各自频次极低。但检查语境发现4次全是同一用户发的帖子属个人造词非社区共识。我的应对策略引入支持度Support要求候选S在至少k个不同文档或用户中出现。k值根据语料规模定10万条以下设k3百万级设k10。计算变异系数CVCV 标准差/均值对S的邻字分布计算CV。若CV0.3说明邻字高度集中如“雪糕刺客”90%配“价格”是真新词若CV0.8说明邻字散乱如“量子”配“力学”“计算”“纠缠”“泡沫”可能是术语泛化非新词。注意不要迷信PMI数值本身。我见过PMI12.5的“赛博朋克风装修”但其freq2且两次都在同一装修论坛帖子中CV0.1但支持度仅1——果断剔除。4.3 中文特有的“嵌套陷阱”为什么“北京大学”不该被拆成“北京”“大学”中文词无空格长词常含短词如“上海交通大学”含“上海”“交通”“大学”。纯统计法会同时输出所有层级造成冗余。我的解法是后处理合并将所有候选按长度升序排列。对每个长度为n的候选S检查是否存在更短候选Tlen(T)n使得S以T开头且T在S中完整出现如S“上海交通大学”T“上海”。若存在且freq(S)/freq(T) 0.3即长词频次远低于短词则标记S为“嵌套项”仅当S的Score T的Score×1.5时才保留S。实测某高校论坛语料“人工智能”freq120“人工”freq850freq_ratio0.14但“人工智能”Score9.2“人工”Score3.19.2 3.1×1.54.65故保留“人工智能”——符合认知。4.4 性能瓶颈突破当语料超千万字时怎么办内存和速度是最大挑战。我的优化方案分块处理将语料按10万行分块每块独立计算字频、邻字分布最后合并统计注意合并时对freq加总对邻字分布做并集。哈希映射不用字符串存候选用hash(text[i:j])作key大幅降低内存占用。并行计算用concurrent.futures.ProcessPoolExecutor并行计算各块的熵/PMI4核CPU下1000万字处理时间从47分钟降至12分钟。实操心得别在单机硬刚亿级语料。我曾试过全量计算进程OOM崩溃3次。后来改用Spark在3节点集群上分发每节点处理300万字20分钟出结果且可复用中间RDD如字频表。5. 效果验证与业务落地如何证明这不是“学术玩具”5.1 客观指标在标准测试集上的硬核表现我选用Bakeoff-2005的MSR语料人民日报分词语料作为黄金标准从中抽取未登录词OOV作为真值。对比三种方法方法PrecisionRecallF1特点纯词频Top1000.320.680.44召回高但精度惨不忍睹TextRank关键词0.510.420.46侧重重要性非新词特性本文熵PMI0.790.730.76平衡性最佳关键洞察我们的方法在长词4-5字召回率上达81%如“供给侧改革”“放管服”远超基线的45%。这是因为熵对长词边界稳定性更敏感。5.2 主观评估业务方眼中的“可用性”技术指标再高业务方说“看不懂”就等于零。我设计了双盲评估步骤1输出TOP100新词混入30个随机干扰词如“的的的”“啊啊啊”请5位非技术人员运营、客服、内容编辑标注“是否认可为当前流行新词”。步骤2统计同意率80%的词为“高共识词”。结果TOP100中72个获高共识包括“电子木鱼”冥想App、“搭子文化”社交关系、“特种兵旅行”高强度打卡游。而被质疑的28个中19个确为误报如“的的确确”被当新词9个是领域黑话如“DAU”需加领域词典过滤。5.3 真实业务场景一个24小时应急响应案例某电商APP在618大促前夜舆情系统报警用户评论中“雪糕刺客”提及量2小时暴涨300%。传统词典更新需T1天而我们的管道00:15接入实时评论流Kafka触发新词提取任务00:18完成清洗、候选生成12万候选00:22计算熵/PMI输出TOP50含“雪糕刺客”Score11.2freq4700:23推送至客服知识库自动关联“价格欺诈”“消费提醒”标签00:25运营后台上线“雪糕刺客避坑指南”弹窗次日复盘该词在用户咨询中占比达18%客服首次响应准确率从62%升至91%。这印证了基于统计的新词挖掘不是替代模型而是为模型争取反应时间的“第一响应者”。6. 延伸思考与进阶方向从“能用”到“好用”的跃迁6.1 动态权重让模型学会“与时俱进”当前Score公式中α、β、γ是静态的但网络热词生命周期不同“绝绝子”爆发快衰减也快而“内卷”则长期稳定。我的进阶方案是引入时间衰减因子Score_t(S) Score(S) × e^(-λ × Δt)其中Δt是S最新一次出现距今的小时数λ根据词类设定如情绪词λ0.05概念词λ0.005。这样“雪糕刺客”在爆发期得分飙升一周后自动降权避免长期污染词表。6.2 跨平台迁移为什么小红书词在知乎上可能失效不同平台用户画像和表达习惯差异巨大。我在小红书语料中提取的“多巴胺穿搭”在知乎技术帖中几乎不出现。解决方案是平台感知的邻字分布分别构建小红书、知乎、B站的独立left_contexts和right_contexts计算时按来源平台选择对应分布。这使跨平台新词召回率提升27%。6.3 与大模型协同统计派的“自我救赎”有人问现在有ChatGLM、Qwen还要手工算熵吗我的答案是要而且更重要。大模型擅长理解但不擅长“发现”。我把熵PMI作为大模型的“前置探针”先用统计法圈出100个高潜力候选再喂给大模型做语义验证如“请判断‘电子布洛芬’是否指代一种缓解精神压力的虚拟物品”。这样计算成本降低90%且大模型只需专注“是/否”判断而非大海捞针。某次实验该组合在1000个候选中精准定位出87个真新词F1达0.89。最后分享一个小技巧每次跑完新词别急着导出。花2分钟扫一遍TOP50的examples列——那些让你忍不住笑出声的语境往往就是下一个破圈词的苗头。我就是在“这届年轻人开始用‘电子木鱼’敲碎KPI”这句话里第一次意识到“电子木鱼”已从佛系梗进化为职场解压符号。技术是骨架而语境里的烟火气才是让新词真正活起来的血肉。