尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

南邮NLP实验一:词典分词与二元语法中文切词实战

南邮NLP实验一:词典分词与二元语法中文切词实战 简介这份资源是南京邮电大学自然语言处理课程实验一的完整实验报告面向正在修读NLP基础课程的高校学生及需要巩固中文分词技术的自学者。报告围绕词典分词与二元语法分词两大核心任务展开涵盖HanLP分词指令操作、词性标注、文件输入输出处理、句法分析以及Python代码实现等环节并对比了前向最长匹配、后向最长匹配与双向最长匹配三种算法的分词差异。资源包内含1个doc文档约232KB结构紧凑完整记录了实验目的、环境配置、原理说明与结果分析。目前已有421人学习下载适合需要撰写实验报告、复习分词算法或熟悉HanLP工具链的读者参考可帮助快速理解中文分词的基本流程与常见歧义处理思路。1. 南邮自然语言处理实验一从词典分词到二元语法一次把中文切词讲透很多同学第一次打开南邮自然语言处理实验一的文档看到“基于词典的分词”“二元语法分词”这几个字第一反应是——不就是把句子切开吗然后动手写了个sentence.split()发现中文根本没有空格直接翻车。这个实验真正要你做的是用两种经典思路把中文句子切成词一种是查词典做最大匹配另一种是用统计语言模型算概率让机器自己判断哪种切法最合理。它解决的是 NLP 最底层的问题——把连续汉字串变成有意义的词序列后面做情感分析、关键词提取、机器翻译全都得先过这一关。适合正在上 NLP 课程、需要交实验报告的同学也适合想补一补中文分词基本功的工程师。下面我按自己踩过的坑把这两条路线从原理到代码完整走一遍。2. 词典分词三种匹配策略与 Python 实现2.1 正向最大匹配为什么容易切出“伪词”正向最大匹配FMM的逻辑非常直白从左到右扫描句子每次取词典中最长词长度的字符窗口看窗口内的串在不在词典里在就切出来不在就缩短窗口继续试。听起来没问题但中文里大量“前缀词”会把它带偏。比如“研究生命起源”如果词典里有“研究”“研究生”“命”“起源”FMM 会先匹配到“研究生”剩下“命起源”再切出“命”和“起源”结果变成“研究生 / 命 / 起源”语义完全变了。正确切法应该是“研究 / 生命 / 起源”。这就是 FMM 的经典缺陷——它只看左边不管右边能不能组成合理的词。我一般会先准备一份词典文件每行一个词用 UTF-8 编码。词典里最好包含常用词和领域词规模不用太大实验阶段几万条就够跑通逻辑。下面是最小可运行的 FMM 实现# 正向最大匹配分词 def fmm_segment(text, word_dict, max_len): text: 待切分的中文字符串 word_dict: set 类型存放词典中的所有词 max_len: 词典中最长词的字数 result [] i 0 while i len(text): # 从最长窗口开始尝试逐步缩短 for length in range(max_len, 0, -1): if i length len(text): continue candidate text[i:ilength] if candidate in word_dict: result.append(candidate) i length break else: # 词典里一个都没匹配上单字成词 result.append(text[i]) i 1 return result # 加载词典 with open(dict.txt, r, encodingutf-8) as f: vocab set(line.strip() for line in f if line.strip()) max_word_len max(len(w) for w in vocab) sentence 研究生命起源 print(fmm_segment(sentence, vocab, max_word_len)) # 输出可能是 [研究生, 命, 起源]暴露 FMM 的缺陷代码里max_len这个参数很关键它决定了每次扫描窗口的上限。设得太小长词永远切不出来设得太大循环次数增加但效果不一定变好。常见做法是取词典中最长词的长度一般不超过 7 到 8 个字。word_dict用set而不是list是因为查找操作从 O(n) 降到 O(1)句子一长差距非常明显。for...else结构保证了一旦匹配成功就跳出内层循环没匹配上才走单字成词的分支。2.2 逆向最大匹配和双向匹配的取舍逆向最大匹配BMM从句子末尾往前扫逻辑和 FMM 对称。它在很多情况下比 FMM 准原因是中文的偏正结构里中心语往往在后面从右往左匹配更容易抓住完整的词。比如“研究生命起源”BMM 从“源”开始往前找会先匹配到“起源”再匹配到“生命”最后剩下“研究”得到正确结果。但 BMM 也不是万能的遇到“中国人民”这种词如果词典里同时有“中国”“人民”“中国人民”BMM 可能切出“中国人民”FMM 也可能切出“中国人民”这时候需要双向匹配来裁决。双向最大匹配的做法是同时跑 FMM 和 BMM然后比较两个结果。裁决规则通常有三条第一词数少的优先因为切出的词越少通常越接近真实语义第二词数相同时单字少的优先第三如果还分不出来就返回 FMM 的结果。这套规则不是理论推导出来的是实践中总结的血泪经验能覆盖大部分歧义场景。def bmm_segment(text, word_dict, max_len): 逆向最大匹配从右往左扫描 result [] i len(text) while i 0: for length in range(max_len, 0, -1): if i - length 0: continue candidate text[i-length:i] if candidate in word_dict: result.append(candidate) i - length break else: result.append(text[i-1]) i - 1 return result[::-1] # 反转回正常顺序 def bidirectional_segment(text, word_dict, max_len): 双向匹配按词数和单字数量裁决 fmm_result fmm_segment(text, word_dict, max_len) bmm_result bmm_segment(text, word_dict, max_len) if len(fmm_result) ! len(bmm_result): return fmm_result if len(fmm_result) len(bmm_result) else bmm_result # 词数相同比较单字数量 fmm_single sum(1 for w in fmm_result if len(w) 1) bmm_single sum(1 for w in bmm_result if len(w) 1) if fmm_single ! bmm_single: return fmm_result if fmm_single bmm_single else bmm_result return fmm_resultbmm_segment里用result[::-1]反转列表是因为从右往左切出来的词序是反的最后要还原。bidirectional_segment的裁决逻辑直接对应上面说的三条规则代码不长但很实用。实际跑的时候你会发现大部分句子 FMM 和 BMM 结果一致只有少数歧义句需要裁决但就是这少数句子决定了分词系统的上限。3. 二元语法分词用概率选最优路径3.1 从“查词典”到“算概率”的思维转换词典分词的死穴在于它只关心“这个词在不在词典里”不关心“这个词出现在这个位置合不合理”。二元语法Bigram分词换了个思路——把所有可能的切分方式都列出来然后算每种切分方式的概率选概率最大的那个。概率怎么算用链式法则拆成相邻词的条件概率乘积。比如句子 ( S w_1 w_2 \dots w_n )它的概率是[ P(S) P(w_1) \cdot P(w_2|w_1) \cdot P(w_3|w_2) \cdots P(w_n|w_{n-1}) ]实际计算时为了防止浮点数下溢通常取对数变成加法[ \log P(S) \log P(w_1) \sum_{i2}^{n} \log P(w_i|w_{i-1}) ]这些概率从哪来从大规模语料里统计。P(w_i|w_{i-1})等于“w_{i-1} 后面跟着 w_i”的次数除以“w_{i-1} 出现的总次数”。实验里一般会给你一份已经统计好的二元语法模型文件格式通常是“词1 词2 概率”或者“词1 词2 频次”。你需要做的就是加载这个模型然后对每种切分路径打分。3.2 构建词图与维特比解码二元语法分词的核心数据结构是词图Word Graph。把句子中所有可能成词的位置连成边每条边带一个词和它的概率然后找一条从起点到终点的最大概率路径。暴力枚举所有路径在句子长的时候会爆炸所以要用维特比算法做动态规划。我一般会先把句子所有可能的词列出来构建一个邻接表然后从右往左递推。下面是一个简化版的实现import math def build_word_graph(text, word_dict, max_len): 构建词图返回每个位置开始的所有可能词 graph {} for i in range(len(text)): graph[i] [] for length in range(1, min(max_len, len(text)-i) 1): word text[i:ilength] if word in word_dict: graph[i].append((ilength, word)) return graph def bigram_segment(text, word_dict, max_len, bigram_prob, unigram_prob): bigram_prob: dict, key 为 (prev_word, word)value 为 log 概率 unigram_prob: dict, key 为 wordvalue 为 log 概率 graph build_word_graph(text, word_dict, max_len) n len(text) # dp[i] 表示从 i 到末尾的最大 log 概率 dp [-float(inf)] * (n 1) dp[n] 0 # 记录路径选择 choice [None] * (n 1) for i in range(n - 1, -1, -1): for end, word in graph[i]: # 计算从 i 开始选这个词的得分 if end n: score unigram_prob.get(word, -20) else: # 找下一个词这里简化处理用 dp[end] 加上转移概率 next_word choice[end][1] if choice[end] else None if next_word: score bigram_prob.get((word, next_word), -20) dp[end] else: score unigram_prob.get(word, -20) dp[end] if score dp[i]: dp[i] score choice[i] (end, word) # 回溯路径 result [] i 0 while i n: end, word choice[i] result.append(word) i end return result这段代码里dp[i]表示从位置 i 到句子末尾的最大对数概率从右往左填表。choice[i]记录在位置 i 选了哪个词、跳到哪个位置最后回溯得到完整切分。bigram_prob和unigram_prob里存的是对数概率所以乘法变成了加法。对于没有在模型里出现的词对给一个很小的默认值比如 -20相当于加了一个平滑防止概率为零导致整条路径被否掉。实际实验里模型文件可能很大加载的时候要注意内存。我一般用dict存二元概率key 用元组(prev_word, word)查起来快。如果模型文件是文本格式逐行解析的时候注意分隔符有的是空格有的是制表符先strip()再split()最稳妥。4. 避坑与排查实验里最容易翻车的五个地方4.1 词典编码不对导致匹配全空现象代码跑起来不报错但所有句子都切成了单字一个多字词都出不来。原因词典文件是 GBK 编码读取时用了 UTF-8或者反过来。Python 读文件时编码不匹配不会抛异常而是读出乱码乱码跟句子里的字符对不上自然全走单字分支。解决统一用 UTF-8。读文件时显式写encodingutf-8如果词典来源不确定先用chardet检测一下或者用open读二进制再手动解码试一次。4.2 最大词长设错导致长词永远切不出现象词典里明明有“中华人民共和国”但切分结果里从来没出现过。原因max_len设成了 4而“中华人民共和国”有 7 个字窗口根本覆盖不到。解决max_len取词典中最长词的长度用max(len(w) for w in vocab)动态计算不要硬编码。如果词典特别大算一次存下来就行不用每次切分都算。4.3 二元语法概率全为零导致路径得分相同现象维特比解码出来的结果跟随机切分差不多完全没有体现概率优势。原因模型文件里的概率值没有取对数直接拿原始概率相乘句子一长结果全是 0.0所有路径得分一样。解决加载模型时立刻取math.log存成对数概率。如果原始概率是 0加一个极小值比如1e-10再取对数避免log(0)报错。4.4 未登录词处理不当导致整句崩盘现象句子里有个人名或新词词典里没有二元语法模型里也没有结果这个词被切得七零八落。原因词典分词遇到未登录词只能单字切二元语法遇到未登录词概率给默认值但如果默认值设得太低整条路径会被带偏。解决给未登录词一个合理的默认概率不要设成负无穷。常见做法是用一个固定的低概率比如 -15 到 -20 之间兜底同时可以在词图构建阶段加入单字候选保证任何字符都有路可走。4.5 评测指标算错导致结果虚高现象自己算的准确率 95%跟同学一对发现别人只有 85%代码逻辑看起来都没问题。原因评测时把标点符号也算进了词或者把空格也当成了切分边界导致分母不对。解决评测前先明确标准标点是否单独成词、数字和英文怎么处理、空格是否计入。跟实验指导书对齐口径别自己发明规则。我一般会先把参考切分和预测切分都做同样的预处理再逐词比对。5. 进阶技巧用混淆矩阵定位分词错误类型实验做完拿到准确率只是第一步真正有价值的是知道错在哪。我习惯把分词错误分成三类切分过多一个词被切成多个、切分过少多个词被合并成一个、边界偏移词数对但边界不对。用一个简单的混淆矩阵就能看清楚错误类型典型例子参考切分预测切分切分过多把“研究”切成“研/究”研究/生命研/究/生命切分过少把“研究/生命”合成“研究生命”研究/生命研究生命边界偏移“研究生/命” vs “研究/生命”研究/生命研究生/命统计的时候逐句对比参考切分和预测切分用集合交并差来归类。切分过多的特征是预测词数大于参考词数切分过少反之词数相同但词不对应就是边界偏移。跑完一遍你会发现词典分词的错误集中在切分过少长词优先导致二元语法的错误集中在边界偏移概率模型对低频词不敏感。调参的方向也因此不同词典分词可以调整max_len或者换用双向匹配来缓解切分过少二元语法可以调整未登录词的默认概率或者引入三元语法让上下文更强。我一般会先跑一遍基线把错误分类统计出来再针对占比最高的那类错误去改比盲目调参效率高得多。还有一个实用技巧把二元语法模型里的高频词对导出来看看前几十个往往能反映出语料的领域特征。如果模型是在新闻语料上训练的拿它切医学文本效果肯定打折。实验里给的模型通常规模有限别指望它能处理所有领域重点是把算法流程跑通、把参数含义搞清楚。我自己的习惯是每改一个参数就存一份结果最后对比着看比来回改代码不记录强得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表