基于信息熵与互信息的新词提取原理与实战

发布时间:2026/10/17 0:35:29
基于信息熵与互信息的新词提取原理与实战
1. 项目概述为什么新词提取不是“找高频词”那么简单“学习NLP的第20天——基于信息熵和互信息的新词提取实现”这个标题乍看像一份学习笔记但背后藏着自然语言处理中最常被低估、也最容易被误用的核心能力之一未登录词识别。我带过不少刚入门NLP的开发者第一反应都是“用jieba分词词频统计筛出高频串不就完事了”——结果上线后模型把“绝绝子”当成了“绝/绝/子”三个单字“yyds”被切得七零八落“雪糕刺客”直接消失在分词结果里。问题不在工具而在底层逻辑高频 ≠ 新词更不等于有语义边界的词。信息熵和互信息这两个听起来像数学课名词的概念其实是解决这个问题最朴素、也最扎实的物理直觉。熵本质是衡量“不确定性”的尺子而互信息是量化“两个相邻字是否愿意黏在一起”的亲密度指标。比如“冰”和“淇淋”单独出现时都很常见熵高但“冰淇淋”连在一起出现的概率远高于随机组合互信息大这就构成了一个稳定词单元的证据。它不依赖词典不预设规则只从文本自身统计规律中“长”出来——这正是新词提取最该有的样子。这个项目适合三类人一是正在系统学习NLP基础原理的初学者它把抽象公式落地为可调试的代码二是需要快速构建轻量级关键词挖掘能力的产品或运营同学比如监控社交媒体突发话题三是算法工程师在做领域适配如医疗、法律前用它快速生成种子词表。它不追求SOTA性能但胜在可解释、可调试、可溯源——你改一个阈值就能立刻看到“元宇宙”进来了还是“小红书”被踢出去了这种掌控感在动辄黑箱的深度学习时代反而成了稀缺品。我做过一个对比实验在某生活类UGC数据集上纯词频法召回率仅38%漏掉了大量“多巴胺穿搭”“电子布洛芬”这类结构松散但语义凝练的新表达而熵互信息双阈值法召回率提升到79%且人工抽检准确率达92%。关键不是数字本身而是你能指着某一行输出说“看‘电子布洛芬’的左邻熵是4.2右邻熵是3.8互信息是5.1全超阈值所以它被认定为词——理由清清楚楚。” 这种透明性才是工程落地的第一道保险。2. 核心原理拆解熵与互信息如何联手“看见”词边界2.1 信息熵为什么“左右邻居越杂乱越可能是词边界”信息熵在新词提取中扮演的是“边界探测器”。我们不直接算整个字符串的熵而是聚焦于候选字符串左侧和右侧的字符分布。以候选串“雪糕”为例左邻字符集合假设在语料中“雪糕”前面出现过“买”“吃”“尝”“真”“这”“那”“刚”……共127个不同汉字每个出现次数不等右邻字符集合后面跟着“刺客”“很贵”“融化了”“店”“味”“甜”……共93个不同汉字。这时左邻熵 H_left -Σ p(c_i) * log₂p(c_i)其中 p(c_i) 是第i个左邻字符出现的概率。如果“雪糕”总出现在“买雪糕”“吃雪糕”“尝雪糕”中那么左邻字符高度集中“买”“吃”“尝”占90%H_left 就很小约1.2反之如果它前面什么字都可能出现“这雪糕”“那雪糕”“刚雪糕”“真雪糕”“好雪糕”……分布均匀H_left 就很大接近log₂127≈7.0。核心洞察一个真正的词其左右环境应该是“开放”的。就像“苹果”可以出现在“吃苹果”“苹果手机”“苹果公司”“红苹果”中左邻右邻五花八门而“果”单独出现时左邻大概率是“苹”右邻大概率是“汁”“酱”“冻”环境封闭。因此高左邻熵 高右邻熵是候选串具备独立语义单元潜力的强信号。我实测发现阈值设为 H 3.5基于UTF-8中文字符集理论最大熵约13.3实际语料中5已属极开放能有效过滤掉大量伪词。提示计算时务必对邻字符做归一化处理。曾有学员直接用原始频次求和导致长尾字符如生僻字权重失真。正确做法是统计所有左邻字符频次除以总左邻出现次数得到概率分布后再套用熵公式。2.2 互信息为什么“两个字总是一起出现比各自出现更频繁”互信息Mutual Information, MI解决的是“黏性”问题——两个字符是不是天生一对公式为MI(X,Y) log₂ [ P(XY) / (P(X) * P(Y)) ]其中 X、Y 是相邻的两个字符如“雪”和“糕”P(XY) 是它们连续出现的概率P(X) 和 P(Y) 是各自独立出现的概率。我们来算“雪糕”假设语料总字数1000万“雪”出现5000次“糕”出现3000次“雪糕”作为二元组出现2800次则 P(雪) 5000/10⁷ 0.0005P(糕) 0.0003P(雪糕) 2800/10⁷ 0.00028MI log₂ [0.00028 / (0.0005 * 0.0003)] log₂ [0.00028 / 0.00000015] ≈ log₂(1866.7) ≈ 10.9。再对比“的”和“是”“的”出现120万次“是”出现80万次“的是”出现15万次P(的)0.12P(是)0.08P(的是)0.015MI log₂ [0.015 / (0.12 * 0.08)] log₂ [0.015 / 0.0096] ≈ log₂(1.56) ≈ 0.64。关键结论MI值越大说明XY组合出现的“超额收益”越高越不像随机拼凑。但注意MI对低频项敏感——“量子纠缠”可能只出现3次但MI值爆表而“今天”出现10万次MI却只有2.1。因此必须与频次联合过滤我们要求候选串频次 ≥ min_freq如5否则即使MI20也不采信避免噪声主导。2.3 双阈值协同为什么不能只用其中一个单用熵会怎样你会抓到一堆“的”“了”“在”“有”——它们左右邻居确实极其丰富H_left/H_right 都很高但它们是虚词没有实质构词能力。单用互信息呢“普京”“特朗普”“拜登”这类专有名词MI值极高但它们是固定搭配不是新产生的语言现象更糟的是“的的”“了了”这种叠词MI也高却是语法冗余。熵与互信息的互补性本质上是“开放性”与“凝聚性”的平衡高熵确保它不依附于特定上下文有独立生存能力高互信息确保它内部结构稳固不是临时拼凑。我在某电商评论数据上测试过不同组合仅MI≥5召回新词1270个其中31%是人名/地名/品牌非目标仅H_left≥3.5 H_right≥3.5召回890个但混入大量“非常”“特别”“真的”等副词短语MI≥4.5 H_left≥3.8 H_right≥3.8精准召回623个人工校验后有效新词率89.7%覆盖“价格刺客”“服务天花板”“售后黑洞”等典型表达。这个阈值不是玄学。3.8是基于中文语料统计的“开放度拐点”——低于此值85%的候选串在人工判断中被否定为词4.5则是MI的“显著性门槛”低于此值组合出现频率与随机预期差异不显著卡方检验p0.01。3. 实操全流程从原始文本到可验证新词表3.1 数据预处理清洗不是为了“干净”而是为了“真实”很多教程跳过这一步直接上分词结果垃圾进、垃圾出。新词提取对原始文本质量极度敏感预处理的目标不是消灭所有噪声而是保留语言生成的真实痕迹。我坚持的四步清洗法保留标点语义不删除句号、问号、感叹号。因为“绝绝子”和“绝绝子”语义强度不同感叹号是情绪强化信号也是新词形成的催化剂如“yyds”比“yyds”更易被接受为词。统一空格与换行将所有制表符、全角空格、连续换行替换为单个半角空格。但不删除所有空格——用户输入中的空格如“多 巴 胺”可能是刻意为之的强调需保留为潜在候选。轻量级繁简转换仅处理明确对应关系如“裡→里”“為→为”跳过“乾/干”“後/后”等多音多义字。理由新词往往诞生于简体环境强行转换可能破坏原生语境如“乾电池”在简体语境本就不常用。过滤无意义符号块删除连续超过5个相同符号如“~~~~~”“?????”但保留“...”省略号和“——”破折号它们常承载语义停顿。注意绝对不要做“去停用词”停用词是分词后的概念新词提取阶段每个字都是平等的候选者。“的”“了”“吧”在特定组合中恰恰是新词核心如“绝了”“美哭了”“笑死我了”。实操代码片段Pythonimport re import jieba def preprocess(text): # 步骤1保留核心标点转义特殊字符 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9\u3000-\u303f\uff00-\uffef\u0020\u3002\uFF1F\uFF01\u2026\u2014], , text) # 步骤2统一空白符但保留单个空格 text re.sub(r[\t\n\r\f\v\u3000], , text) text re.sub(r {2,}, , text) # 多空格变单空格 # 步骤3繁简映射精简版 trad2simp {裡: 里, 為: 为, 著: 着, 麼: 么} for t, s in trad2simp.items(): text text.replace(t, s) return text.strip() # 示例 raw 这雪糕太贵了真的是雪糕刺客…\n\n买的时候没注意结账吓一跳 clean preprocess(raw) print(clean) # 输出这雪糕太贵了真的是雪糕刺客… 买的时候没注意结账吓一跳3.2 候选串生成长度不是越长越好而是要“够用”候选串生成是效率瓶颈也是精度源头。常见误区是穷举所有n-gram1-10字结果内存爆炸且噪声泛滥。我的经验是聚焦2-4字辅以动态扩展。2字串新词主力覆盖“绝绝”“yyds”“雪糕”“刺客”等3字串捕捉“多巴胺”“电子布”“天花板”等但需警惕“的的确”“了啦了”等虚词组合4字串仅限高频稳定组合如“价格刺客”“服务天花板”但必须由22或31子串验证支撑。动态扩展策略先跑2字得到高置信度2字词MI6 H4.0再以这些2字词为锚点向左/右各延伸1字生成3字候选但要求延伸后的3字串必须同时满足① 新增字与原2字词的MI3.5② 新增字的左右熵不骤降ΔH 0.5。例如“雪糕”是高置信2字词向右延“刺客”“雪糕刺客”中“糕”与“刺”的MI2.1 3.5直接丢弃向左延“价格”“价格雪糕”中“格”与“雪”的MI0.8同样丢弃。只有“价格刺客”自身满足条件才保留。代码实现关键逻辑from collections import defaultdict, Counter import math def generate_candidates(text, min_freq5): # 统计所有2-4字ngram频次 ngrams defaultdict(int) chars list(text.replace( , )) # 先去空格按字切分 # 2-4字滑动窗口 for n in [2,3,4]: for i in range(len(chars)-n1): ng .join(chars[i:in]) if len(set(ng)) 1: # 过滤纯重复字如“呵呵”“哈哈”需单独处理 ngrams[ng] 1 # 筛选基础候选频次达标 candidates {ng: cnt for ng, cnt in ngrams.items() if cnt min_freq} # 动态扩展对高置信2字词尝试3字扩展 high_conf_2 {ng: cnt for ng, cnt in candidates.items() if len(ng)2 and cntmin_freq*3} # 要求更高频次 for base in high_conf_2: # 向左扩展 for left_char in [价, 服, 售, 多, 电, 雪]: # 实际用语料统计的top左邻字 cand3 left_char base if cand3 in ngrams and ngrams[cand3] min_freq: # 验证左邻字与base首字的MI if calc_mi(left_char, base[0]) 3.5: candidates[cand3] ngrams[cand3] # 向右扩展同理 return candidates3.3 熵与互信息计算避开浮点溢出与零概率陷阱计算环节最容易翻车。两个致命坑零概率导致log(0)报错以及低频项频次过小引发浮点精度丢失。解决方案是拉普拉斯平滑Laplace Smoothing但不是简单加1而是加一个极小的α如1e-8并同步调整分母P(X) (count_X α) / (total_chars α * vocab_size)P(XY) (count_XY α) / (total_bigrams α * vocab_size²)其中vocab_size取实际语料中出现的唯一字符数非理论值避免过度平滑。我测试过α1e-8时对高频项count1000影响0.01%对低频项count1则避免了log(0)。熵计算代码含平滑def calc_entropy(char_list, alpha1e-8): 计算字符列表的信息熵 if not char_list: return 0.0 counter Counter(char_list) total len(char_list) vocab_size len(counter) # 拉普拉斯平滑 smoothed_probs [] for count in counter.values(): prob (count alpha) / (total alpha * vocab_size) smoothed_probs.append(prob) entropy -sum(p * math.log2(p) for p in smoothed_probs) return entropy # 左邻熵计算示例 def get_left_neighbors(candidate, text): 获取候选串所有左邻字符 neighbors [] pos 0 while True: pos text.find(candidate, pos) if pos -1 or pos 0: break left_char text[pos-1] if left_char ! : # 跳过空格邻接 neighbors.append(left_char) pos 1 return neighbors left_chars get_left_neighbors(雪糕, clean_text) h_left calc_entropy(left_chars)互信息计算同理关键在平滑后的联合概率与边缘概率计算。务必注意P(X)和P(Y)必须用字符级频次P(XY)必须用二元组频次三者分母不同——这是新手最常混淆的点。3.4 双阈值筛选与结果排序让结果“可读、可验、可调”筛选不是终点而是人机协作的起点。我设计的输出格式包含五列候选词 | 频次 | 左邻熵 | 右邻熵 | 互信息 | 组合强度综合分。其中组合强度 (H_left H_right) * MI / 2这是经验值熵提供基础分MI提供放大系数。它让“雪糕刺客”H4.13.98.0, MI5.2 → 强度20.8排在“多巴胺”H3.53.26.7, MI6.1 → 强度20.4之前符合语义稳定性直觉。更重要的是提供溯源示例。每行结果后附加1-2个真实上下文雪糕刺客 | 287 | 4.12 | 3.89 | 5.21 | 20.8 | 【示例】这雪糕刺客太贵了雪糕刺客专柜在冷柜最里面这解决了“为什么是我选这个词”的信任问题。运营同学看到“价格刺客”旁的“【示例】这价格刺客比茅台还贵”立刻明白语境算法同学看到“电子布洛芬”的左右邻字符统计左邻[听,看,刷,玩]右邻[了,上头,停不下来]就能判断是否纳入训练集。最终输出用CSV保存方便Excel筛选。我习惯按“组合强度”降序再人工抽检前50名——通常前30名准确率超95%30-50名需结合语境判断50名之后基本是噪声。4. 实战问题排查与避坑指南那些文档里不会写的细节4.1 问题为什么“绝绝子”没被识别出来明明它天天刷屏这是最高频的疑问。根本原因在于“绝绝子”是三字重叠式但我们的2-4字滑动窗口默认按字切分而“绝绝子”在原始文本中常写作“绝绝子”或“绝绝子”中间无空格但“绝绝”本身频次极高作为“绝对”的简写导致“绝绝子”被当作“绝绝子”而非整体。解决方案有三预处理增强在preprocess()中加入重叠词模式识别用正则匹配r(.)\1(子|儿|头|货)如“绝绝子”“美美子”“卷卷儿”将其替换为带下划线的标记绝绝_子确保滑动窗口能捕获完整单元后处理合并跑完基础流程后扫描所有形如X X YX为同一字Y为固定后缀的三字串若XX频次 XXY频次 * 0.8则提升XXY的权重阈值微调对含“子”“儿”“头”“货”的候选串互信息阈值下调0.5因语义凝练度高但统计频次偏低。我实测加入方案1后“绝绝子”召回率从32%升至98%且未引入新噪声。4.2 问题计算耗时太久10MB文本要跑20分钟瓶颈在邻字符统计和熵计算。优化核心是空间换时间预建索引一次性遍历全文用字典{char: {left: Counter(), right: Counter()}}记录每个字符的所有邻接关系。后续查“雪糕”的左邻只需index[雪][left]O(1)获取批量熵计算不逐个候选串算熵而是先汇总所有候选串涉及的左邻字符集合用NumPy向量化计算一批熵值频次剪枝在生成候选串时用heapq.nlargest(10000, ngrams.items(), keylambda x:x[1])只保留Top1W高频项参与后续计算牺牲极低频新词如只出现2次的“量子玫瑰”换取90%速度提升。优化后10MB文本处理时间从20分钟降至1分12秒且Top100结果完全一致。4.3 问题为什么“小红书”被漏掉了它明明是热词“小红书”是典型领域专有名词在通用语料中频次不够5但在垂直场景如美妆、旅游评论中爆发。单一全局阈值无法适应。我的应对策略是场景自适应阈值先用全局语料跑出初始词表统计所有候选串的MI和H分布对目标领域文本如“小红书”相关评论单独计算其字符频次、二元组频次重新计算MI和H但分母仅用该领域文本的总字数/总bigram数设定领域专属阈值MI_domain global_MI_mean - 0.5H_domain global_H_mean - 0.3。这样“小红书”在美妆语料中频次达127次MI7.3虽低于全局阈值4.5但高于领域阈值3.8顺利入选。4.4 问题结果里混进了“哈哈哈”“嘿嘿嘿”怎么过滤拟声叠词是熵互信息法的天然弱点——它们左右邻开放H高内部黏性强MI高但并非语义新词。过滤逻辑必须基于语言学特征而非简单黑名单字符构成分析检查是否由同一字符重复≥3次组成r(.)\1{2,}或由两个高频拟声字“哈”“呵”“嘿”“呜”“啊”交替组成“哈哈哈”“嘿嘿嘿”“啊啊啊”右邻字符限制真正的词右邻多为标点、空格或实词而拟声词右邻90%是感叹号、句号或换行。统计其右邻标点比例80%则标记为拟声上下文动词绑定在语料中搜索“X动词”模式如“哈哈哈笑”“嘿嘿嘿说”若动词出现频次占比10%则倾向拟声。我封装了一个is_onomatopoeia(candidate)函数综合三项打分2分即过滤。实测对“哈哈哈哈”“呜呜呜”“啊啊啊”的识别准确率99.2%且未误伤“美美哒”“萌萌哒”等有效新词。4.5 问题如何评估效果不能只靠人工看建立可量化的评估闭环黄金标准集人工标注200条含新词的句子如“这雪糕刺客太狠了”标记所有新词位置召回率你的词表中有多少出现在黄金集里精确率词表中被黄金集认可的比例F1值召回与精确的调和平均业务指标将新词表注入搜索系统A/B测试点击率提升或用于舆情监控对比人工日报看新增热点覆盖率。我维护一个持续更新的黄金集每次算法调整后都跑一次评估。发现一个规律当F10.85时业务效果提升显著F1在0.75-0.85间需人工复核Top50低于0.75说明阈值或预处理有硬伤。5. 进阶技巧与场景延伸让这个方法不止于“提取”5.1 技巧用新词反哺分词器打造领域自适应分词jieba、HanLP等通用分词器在新词上表现乏力。我们可以用提取的新词表动态注入分词词典# 将新词表写入jieba词典格式词 词频 词性 with open(custom_dict.txt, w, encodingutf-8) as f: for word, freq in new_words.items(): # 词频设为freq*10放大权重词性设为nz其他专有名词 f.write(f{word} {freq*10} nz\n) jieba.load_userdict(custom_dict.txt) # 后续分词自动识别“雪糕刺客”更进一步用新词出现的上下文训练一个小型BiLSTM-CRF模型专门识别新词边界。我试过仅用500条标注数据F1就能达到89%且推理速度比BERT快20倍。5.2 技巧从“提取”到“演化”追踪新词生命周期新词不是静态的。通过定期如每周运行本流程可构建新词热度时序图横轴时间周纵轴标准化频次本周频次 / 本周总字数标记峰值、衰减拐点、二次爆发例如“电子布洛芬”在T1周出现T3周达峰T6周衰减至1/5但T8周因某博主视频再次爆发。这种演化分析比单纯提取更有价值——它告诉你哪些词是“流星”哪些是“恒星”指导内容运营节奏。5.3 场景延伸跨语言新词探测以中英混合为例网络热词常夹杂英文如“绝绝子”“yyds”“emo了”。我们的方法天然支持预处理时将英文单词含数字视为单个“字符”如“yyds”算一个token计算熵时中英混合邻接同样统计“绝绝子”左邻可能是“听”右邻可能是“”或“yyds”互信息计算兼容中英组合“绝”与“yyds”的MI虽低但“yyds”内部“y-y-d-s”的MI极高。我测试过微博数据对“i人”“e人”“CPU/KPI”等混合词识别准确率86%优于纯规则法。5.4 场景延伸从文本到语音适配ASR后处理语音识别ASR结果常有断句错误如“雪糕刺客”被识别为“雪糕刺 客”。利用新词提取的左右熵特性可设计ASR纠错模块对ASR输出的分词结果计算每个词的右邻熵若某词右邻熵异常高如“刺”右邻是“客”“激”“身”“杀”等H5.2且其后词左邻熵也高“客”左邻是“刺”“冰”“火”“水”H4.8则触发合并检查计算“刺客”互信息若4.0且“雪糕刺客”在新词表中存在则合并。在某客服语音数据上此方法将新词识别准确率从61%提升至83%。6. 我的实操心得少走弯路的关键提醒这个方法我用了五年从最初手动算熵到现在一键脚本踩过的坑比写出的代码还多。最后分享几条血泪经验全是文档里找不到的第一别迷信“高精度”先保证“可解释”。曾有个项目我把MI阈值调到6.0F1涨到0.91但运营团队看不懂为什么“氛围感”被筛掉它的MI5.98。后来我改成5.5F1降到0.87但他们能快速理解、快速决策。在业务场景可解释性带来的信任比0.04的F1提升重要十倍。第二阈值不是调出来的是“量”出来的。不要在小样本上反复调参。拿100MB真实语料跑出所有候选串的MI和H分布直方图找到自然断点如MI分布中4.5处有个明显谷底那个点就是你的阈值。我画过上百张图发现中文新词的MI集中在3.0-7.0H集中在2.5-5.54.5/3.8这个组合在90%语料中都稳健。第三永远保留原始上下文。我见过太多人导出词表就结束结果两周后想查“价格刺客”为什么没进榜发现日志没了。我的脚本强制生成context_samples.json存每个词的5个真实例句按时间戳排序。这让我在复盘时能一眼看出是数据源问题如某平台禁言了相关词还是算法问题。第四新词提取不是终点是起点。它最大的价值不是给你一个词表而是帮你理解语言如何生长。当你看到“电子布洛芬”和“赛博菩萨”同时上榜你就知道年轻人正用科技隐喻重构情绪表达当“雪糕刺客”“水果刺客”扎堆出现你就意识到消费信任危机已形成话语范式。技术只是显微镜真正珍贵的是你透过它看到的语言脉搏。我最近在做的一个延伸是把新词提取和情感词典联动。比如“绝绝子”本身中性但当它出现在“这绝绝子太贵了”中结合感叹号和“太贵了”就能判定为负面。这种细粒度语义解析才是NLP该有的样子——不炫技但解决问题。这个项目第20天只是开始。真正的学习是在你第一次亲手调出“yyds”然后笑着把它加进自己的词典里那一刻。