ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

南邮NLP实验一:HanLP中文分词与二元语法实战 简介这份资源是南京邮电大学自然语言处理课程实验一的完整实验报告面向正在学习NLP基础课程的高校学生及需要完成分词实验的初学者。报告围绕词典分词与二元语法分词两大核心任务展开涵盖HanLP工具的分词指令、句法分析指令、文件输入输出处理以及前向最长匹配、后向最长匹配、双向最长匹配三种算法的对比分析并附有Python代码实现示例。资源包为1个doc文档大小约232KB内容包含实验目的、环境配置、原理说明、操作步骤与结果记录结构完整可直接作为实验参考模板。目前已有421人学习下载适合需要快速理解分词原理、对照完成实验报告或复习HanLP用法的读者使用。1. 从南邮实验一拆开中文分词词典匹配和二元语法到底怎么落地很多人第一次接触中文 NLP都是从“分词”这两个字开始的。看起来简单——把一句话切成词——但真动手写代码你会发现坑比想象中多得多。南邮自然语言处理实验一就是围绕这个核心问题展开的用 HanLP 做词典分词和二元语法分词跑通从命令行到 Python 调用的完整链路。这份实验报告覆盖了 HanLP 的 segment、parse 指令、前向/后向/双向最长匹配算法、DoubleArrayTrie 和 AhoCorasickDoubleArrayTrie 两种词典结构、停用词过滤以及基于自己构建的小语料训练二元语法模型并用 ViterbiSegment 做分词预测。适合正在上 NLP 课程、需要交实验报告的同学也适合想快速摸清 HanLP 分词能力边界、不想翻厚重文档的工程师。下面我按实际动手顺序把每一步拆开讲清楚。2. HanLP 环境搭建与命令行分词从 hanlp segment 到文件批量处理2.1 安装 HanLP 与验证环境实验环境要求 Windows Python 3.7 或 3.8。HanLP 的 Python 接口依赖 pyhanlp 包底层是 Java 实现所以需要 JDK 环境。常见做法是先用 conda 建一个干净环境再装 pyhanlpconda create -n nlp_lab python3.8 conda activate nlp_lab pip install pyhanlp装完之后第一次运行from pyhanlp import *时pyhanlp 会自动下载 data 包大约几百 MB包括核心词典、二元语法模型等。如果网络不稳定这一步容易卡住。我一般会先手动确认HanLP.Config.CoreDictionaryPath指向的路径下文件是否完整。验证安装是否成功from pyhanlp import * print(HanLP.segment(商品和服务))如果输出[商品/n, 和/cc, 服务/vn]说明环境没问题。注意词性标注默认是开启的/n表示名词/cc表示并列连词/vn表示动名词。这些词性标签来自 HanLP 内置的 ICTCLAS 兼容标签集。2.2 命令行分词指令与参数说明HanLP 安装后会注册一个命令行入口hanlp。直接在终端输入hanlp可以看到所有子指令。实验里重点用到两个segment和parse。hanlp segment进入交互模式输入句子回车即可看到分词结果。如果不想输出词性加--no-taghanlp segment --no-tag批量处理文件时用重定向hanlp segment input1.txt output1.txt -a crf这里-a crf指定使用 CRF 模型做分词而不是默认的 Viterbi。CRF 在未登录词和歧义切分上通常更稳但速度慢一些。实验报告里对“春分最具灵性的美……”这段文本的输出结果就是 CRF 模型跑出来的。你可以对比不加-a crf的结果会发现某些边界切分不一样。提示如果hanlp命令找不到检查 pyhanlp 安装后 Scripts 目录是否在 PATH 里或者直接用python -m pyhanlp替代。2.3 句法分析指令 parse 的输出解读hanlp parse做的是依存句法分析输出的是每个词与其他词的依存关系。实验里对“徐先生还具体帮助他确定了把画雄鹰、松鼠和麻雀作为主攻目标”的分析结果每行格式是序号、词语、词性、依存弧、关系类型。比如第 4 行“帮助”是核心关系_0说明它是整句的根节点。第 1 行“徐先生”指向第 4 行关系是“主谓关系”。第 5 行“他”也指向第 4 行关系是“兼语”——因为“帮助他确定”里“他”既是“帮助”的宾语又是“确定”的主语。这个输出对理解句子结构很有用但要注意HanLP 的依存分析基于预训练模型对复杂长句不一定百分百准确。实验里这个句子结构清晰所以结果比较规整。实际用的时候如果句子里有省略或倒装依存弧可能会乱。3. 词典分词三种匹配算法前向、后向、双向的代码实现与差异3.1 最长匹配法的核心逻辑词典分词的本质是给定一个词典对句子做切分使得切分结果尽量合理。最长匹配法是最直观的策略——每次从当前位置开始找词典里能匹配的最长词。前向最长匹配从左到右扫描每次取最长匹配。 后向最长匹配从右到左扫描每次取最长匹配。 双向最长匹配同时跑前向和后向然后按规则选一个更优的结果。常见规则是词数更少优先词数相同则单字更少优先。实验里对“结婚的和尚未结婚的”这个经典歧义句三种方法结果不同前向[结婚, 的, 和尚, 未, 结婚, 的]后向[结婚, 的, 和, 尚未, 结婚, 的]双向[结婚, 的, 和, 尚未, 结婚, 的]前向匹配把“和尚”切出来了但语义上“和/尚未”更合理。后向匹配避免了这个问题。双向匹配选了后向的结果。3.2 Python 实现三种匹配算法下面是我自己写的一版不依赖 HanLP 的词典用一个小词典演示# 词典实际使用时可以从文件加载 word_dict {结婚, 的, 和, 和尚, 尚未, 未, 结, 婚} def forward_max_match(text, dictionary): result [] i 0 while i len(text): matched False # 从最长可能长度开始尝试 for j in range(min(len(text) - i, 5), 0, -1): if text[i:ij] in dictionary: result.append(text[i:ij]) i j matched True break if not matched: result.append(text[i]) i 1 return result def backward_max_match(text, dictionary): result [] i len(text) while i 0: matched False for j in range(min(i, 5), 0, -1): if text[i-j:i] in dictionary: result.append(text[i-j:i]) i - j matched True break if not matched: result.append(text[i-1]) i - 1 return result[::-1] def bidirectional_max_match(text, dictionary): fwd forward_max_match(text, dictionary) bwd backward_max_match(text, dictionary) # 简单规则词数少优先词数相同单字少优先 if len(fwd) ! len(bwd): return fwd if len(fwd) len(bwd) else bwd fwd_single sum(1 for w in fwd if len(w) 1) bwd_single sum(1 for w in bwd if len(w) 1) return fwd if fwd_single bwd_single else bwd text 结婚的和尚未结婚的 print(前向:, forward_max_match(text, word_dict)) print(后向:, backward_max_match(text, word_dict)) print(双向:, bidirectional_max_match(text, word_dict))参数说明max_len我设的是 5实际词典里最长词可能更长需要根据词典调整。word_dict用 set 存储查找是 O(1)。双向匹配的选优规则可以改比如有些实现会优先选后向结果。3.3 DoubleArrayTrie 与 AhoCorasickDoubleArrayTrie 的差异实验里用 HanLP 的DoubleArrayTrieSegment和AhoCorasickDoubleArrayTrieSegment对同一段文字分词结果差异很大。前者输出的是正常词语后者几乎切成了单字。原因在于DoubleArrayTrieSegment加载的是 HanLP 核心词典词典里有“春分”“特别”“均分”这些词所以能正确切分。而AhoCorasickDoubleArrayTrieSegment如果不加载自定义词典默认词典为空所以每个字都单独成词。正确用法是给 AhoCorasick 也加载词典from pyhanlp import * # 方式一DoubleArrayTrieSegment默认加载核心词典 seg1 DoubleArrayTrieSegment() print(seg1.seg(春分之特别在一个“均”字)) # 方式二AhoCorasickDoubleArrayTrieSegment需要手动加载词典 AhoCorasickDoubleArrayTrieSegment JClass(com.hankcs.hanlp.seg.Other.AhoCorasickDoubleArrayTrieSegment) seg2 AhoCorasickDoubleArrayTrieSegment() seg2.enableCustomDictionary(True) # 启用自定义词典 print(seg2.seg(春分之特别在一个“均”字))AhoCorasick 的优势在于多模式匹配适合从文本中同时查找多个关键词。如果只是做通用分词DoubleArrayTrie 更合适。4. 二元语法分词实战从语料训练到 ViterbiSegment 预测4.1 构建语料与训练二元语法模型实验要求新建my_cws_corpus.txt内容如下商品 和 服务 商品 和服 物美价廉 服务 和 货币然后训练二元语法模型from pyhanlp import * CorpusLoader SafeJClass(com.hankcs.hanlp.corpus.document.CorpusLoader) NatureDictionaryMaker SafeJClass(com.hankcs.hanlp.corpus.dictionary.NatureDictionaryMaker) def train_bigram(corpus_path, model_path): sents CorpusLoader.convert2SentenceList(corpus_path) for sent in sents: for word in sent: word.setLabel(n) # 统一词性为名词简化处理 maker NatureDictionaryMaker() maker.compute(sents) maker.saveTxtTo(model_path) train_bigram(my_cws_corpus.txt, my_cws_corpus_model)运行后会生成三个文件文件名作用my_cws_corpus_model.txt一元语法模型每行格式单词 词性 频次my_cws_corpus_model.tr.txt词性标注相关数据my_cws_corpus_model.ngram.txt二元语法模型记录词对共现频次一元模型里“商品”出现 2 次“和”出现 2 次“服务”出现 2 次。二元模型里会记录“商品 和”“和 服务”等词对的频次。4.2 加载模型并查询词频训练完模型后需要告诉 HanLP 用我们自己的模型而不是默认模型from pyhanlp import * def load_bigram(model_path): HanLP.Config.CoreDictionaryPath model_path .txt HanLP.Config.BiGramDictionaryPath model_path .ngram.txt CoreDictionary SafeJClass(com.hankcs.hanlp.dictionary.CoreDictionary) print(商品: , CoreDictionary.getTermFrequency(商品)) print(和: , CoreDictionary.getTermFrequency(和)) load_bigram(my_cws_corpus_model)输出是商品: 2和和: 2。注意这里修改的是全局配置会影响后续所有分词操作。如果后面还要用默认模型记得改回来或者用独立进程跑。4.3 ViterbiSegment 分词与结果解读加载自定义模型后用 ViterbiSegment 对“商品和服务”分词from pyhanlp import * def predict(): HanLP.Config.CoreDictionaryPath my_cws_corpus_model.txt HanLP.Config.BiGramDictionaryPath my_cws_corpus_model.ngram.txt ViterbiSegment JClass(com.hankcs.hanlp.seg.Viterbi.ViterbiSegment) segment ViterbiSegment() s segment.seg(商品和服务) print(s) predict()输出[商品/n, 和/n, 服务/n]。为什么“和”被单独切出来了因为训练语料里“商品 和 服务”是一条完整句子“和”作为独立词出现。而“和服”虽然在另一条语料里出现但“商品 和服 物美价廉”里“和服”是一个词。Viterbi 算法会根据二元语法概率选择最优路径。在这个小语料下“商品/和/服务”的路径概率更高。这个实验的关键点是二元语法分词的效果高度依赖训练语料的规模和覆盖度。三条语料只能演示流程实际场景需要大量标注数据。5. 避坑与排查HanLP 实验里最容易翻车的五个地方5.1 UnicodeDecodeError: gbk codec cant decode现象读取停用词表或语料文件时Python 报UnicodeDecodeError: gbk codec cant decode byte 0x90。原因Windows 默认编码是 GBK但文件实际是 UTF-8 编码。open()不指定 encoding 时Python 用系统默认编码去解码遇到 UTF-8 字节就崩了。解决所有文件读取都显式加encodingutf-8with open(path, encodingutf-8) as src: for word in src: word word.strip() # ...5.2 pyhanlp 首次运行卡在下载 data 包现象from pyhanlp import *执行后长时间无响应或者报连接超时。原因pyhanlp 需要下载几百 MB 的 data 包默认从 GitHub 拉取网络不稳定时容易失败。解决手动下载 data 包放到 pyhanlp 的 static 目录下。或者设置环境变量HANLP_DATA_PATH指向已下载的 data 目录。如果之前装过检查HanLP.Config.CoreDictionaryPath指向的路径是否存在。5.3 修改全局配置后默认模型失效现象跑完二元语法实验后再用HanLP.segment()分词结果变得很奇怪很多词被切成单字。原因HanLP.Config.CoreDictionaryPath和BiGramDictionaryPath被改成了自定义模型路径但自定义模型只包含三条语料的词汇覆盖不了通用文本。解决实验结束后恢复默认配置或者把自定义模型相关的代码放在独立脚本里跑。如果要在同一进程里切换先保存原始路径original_core HanLP.Config.CoreDictionaryPath original_bigram HanLP.Config.BiGramDictionaryPath # ... 做实验 ... HanLP.Config.CoreDictionaryPath original_core HanLP.Config.BiGramDictionaryPath original_bigram5.4 AhoCorasickDoubleArrayTrieSegment 输出全是单字现象用 AhoCorasick 分词结果每个字都是独立的没有词语。原因AhoCorasick 默认不加载任何词典需要手动启用自定义词典或加载核心词典。解决调用enableCustomDictionary(True)或者用DoubleArrayTrieSegment替代。如果确实需要 AhoCorasick 的多模式匹配能力先加载词典再分词。5.5 停用词过滤后标点符号也被替换现象用停用词表过滤文本结果标点符号也被替换成了*。原因HanLP 的核心停用词表里包含标点符号。如果不想过滤标点需要自己维护一个停用词表去掉标点。解决加载停用词表后手动移除标点符号或者用自定义停用词表# 过滤掉停用词表中的标点 stopwords set() with open(HanLP.Config.CoreStopWordDictionaryPath, encodingutf-8) as f: for line in f: word line.strip() if word and not word in 。、: stopwords.add(word)6. 进阶技巧用自定义词典和模型验证分词效果实验里用到的 DoubleArrayTrie 和 AhoCorasick 都是词典分词的底层结构。实际项目中更常见的做法是用通用模型做基础分词再用自定义词典补充领域词汇。HanLP 支持通过CustomDictionary动态添加词from pyhanlp import * # 添加自定义词 CustomDictionary.add(春分二候) CustomDictionary.add(玉渊潭) text 走在春分二候的京城植物园的玉兰在蓝天下盛开 print(HanLP.segment(text))如果不加自定义词“春分二候”可能被切成“春分/二候”或更碎。加了之后HanLP 会优先匹配自定义词。另一个实用技巧是用HanLP.segment的Segment类手动指定模型和词典避免全局配置污染from pyhanlp import * Segment JClass(com.hankcs.hanlp.seg.Segment) segment Segment.newSegment() # 或者指定自定义词典 segment.enableCustomDictionary(true) result segment.seg(商品和服务) print(result)验证分词效果时我一般会准备一个小测试集包含歧义句、未登录词、数字和英文混合等情况跑一遍看切分边界是否符合预期。比如“结婚的和尚未结婚的”这种句子就是检验分词器歧义处理能力的试金石。从那以后我每次跑 HanLP 实验都会先把全局配置备份一遍实验结束立刻恢复避免后面调用默认模型时出现玄学问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表