恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

HanLP词典分词与二元语法分词实验:从代码实现到避坑指南

  • 首页
  • 资讯中心
  • /
  • HanLP词典分词与二元语法分词实验:从代码实现到避坑指南

相关资讯

OpenCode报错排查:console provider免费额度使用限制详解 2026/10/16 17:58:37
MATLAB统计工具箱实战:线性回归与K均值聚类全流程 2026/10/16 17:58:37
Qoder实测:Codex平替AI编程IDE,从安装到Spring Boot调试 2026/10/16 17:58:37

最新资讯

EO.WebBrowser:.NET WinForms项目替换IE内核的Chromium浏览器控件
VMware Linux虚拟机磁盘扩容实战:fdisk分区挂载与fstab配置全解析
小票打印源码解析:ESC/POS指令、Python实现与排版避坑指南
6.6kW双向DAB CLLC参考设计:从参数计算到调试避坑
腾讯乐加固工具包:APK加密、重签名与防逆向实战指南
Spring Boot项目集成实战:A访问B的Controller层的四种主流方案

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

HanLP词典分词与二元语法分词实验:从代码实现到避坑指南

发布时间:2026/10/16 17:58:37
HanLP词典分词与二元语法分词实验:从代码实现到避坑指南 简介本资源为南京邮电大学自然语言处理课程实验一的完整实验报告面向修读NLP基础课程的高校学生及希望入门中文分词与句法分析的初学者。报告围绕词典分词与二元语法分词两大核心任务展开涵盖HanLP分词指令segment、句法分析parse、前向/后向/双向最长匹配法对比以及DoubleArrayTrieSegment与AhoCorasickDoubleArrayTrieSegment的Python实现并附有教材第27页代码示例与详细输出结果。资源包共1个doc文件约232KB内容为结构完整的实验报告文档包含实验目的、环境配置、原理说明、操作步骤与结果记录可直接对照复现实验流程。目前已有424人学习下载适合需要完成同类实验、理解中文分词算法差异或熟悉HanLP工具链的读者参考借鉴。1. 词典分词与二元语法分词一份能直接跑通的实验拆解很多人第一次接触中文分词都是从“结婚的和尚未结婚的”这种经典歧义句开始的。这份实验资源围绕 HanLP 工具包把词典分词和二元语法分词两条主线串了起来从命令行调用到 Python 代码实现再到自定义语料训练模型覆盖了中文分词入门阶段最核心的几个操作。它适合正在上自然语言处理课程、需要交实验报告的学生也适合想快速摸清 HanLP 分词接口的开发者。整份材料最值钱的地方不是概念讲解而是那些可以直接复制运行的代码片段和参数配置——你照着敲一遍就能看到分词结果从“翻车”到“勉强能用”的全过程。2. 环境搭建与 HanLP 命令行初探从安装到第一条分词指令2.1 环境准备与 HanLP 安装确认实验环境写的是 Windows Python 3.7 或 3.8这个版本区间不是随便写的。HanLP 的 Python 封装 pyhanlp 依赖 JPype而 JPype 在 Python 3.9 以上版本经常出现兼容性问题所以如果你用的是 3.10 或更高版本大概率会在import pyhanlp这一步就翻车。我一般会建议直接用 conda 建一个 3.8 的虚拟环境省去后面折腾依赖的时间。安装命令本身不复杂但要注意 pyhanlp 会自动拉取 HanLP 的 jar 包和基础模型首次运行时会从远程下载数据网络不稳的话容易卡住。常见做法是先用 pip 装好然后跑一个最简单的分词测试确认模型文件已经就位。# 创建并激活 Python 3.8 虚拟环境 conda create -n hanlp_env python3.8 conda activate hanlp_env # 安装 pyhanlp pip install pyhanlp # 验证安装首次运行会自动下载模型耐心等待 python -c from pyhanlp import HanLP; print(HanLP.segment(商品和服务))这段代码的逻辑很直接建环境、装包、跑一条分词。关键参数在HanLP.segment()里传入的字符串就是待分词文本返回的是一个 Term 列表每个 Term 包含词语和词性。如果你看到输出里有/n、/cc这样的标记说明词性标注也正常工作了。首次运行如果卡在下载环节可以检查~/.hanlp目录下是否有 hanlp-release 文件夹没有的话就是没下载完。2.2 命令行分词指令与文件输入输出HanLP 装好之后命令行工具hanlp就可以直接用了。实验里要求先输入hanlp segment进入交互模式然后逐句测试。这个交互模式的好处是你可以连续输入多句话不用每次都重新加载模型。但实际用起来更常见的是通过文件重定向做批量分词毕竟一句一句敲效率太低。# 交互模式输入后回车CtrlC 退出 hanlp segment # 文件输入输出对 input1.txt 分词结果写入 output1.txt hanlp segment input1.txt output1.txt -a crf # 不输出词性只要分词结果 hanlp segment --no-tag input1.txt output1.txt这里有几个参数值得说清楚。-a crf指定的是分词算法crf 表示条件随机场模型比默认的 Viterbi 算法在未登录词识别上更稳一些但速度会慢一点。--no-tag是去掉词性标注输出更干净。重定向符号和是 shell 的基本操作前者把文件内容喂给程序后者把程序输出写到文件。如果你在 Windows 的 cmd 里跑这些符号同样适用但路径要用反斜杠或者双引号包起来。实验里给的输出示例中标点符号也被单独切分并标注为/w这是 HanLP 的默认行为。如果你不想要标点可以在后处理里过滤掉词性为w的 Term。另外注意hanlp segment默认用的是 Viterbi 算法加-a crf才会切换到 CRF 模型两者在歧义句上的表现差异后面会具体说。2.3 句法分析指令 parse 的调用方式句法分析在实验里只占了一小部分但它是理解句子结构的关键。hanlp parse指令输出的是依存句法树每一行包含词序号、词语、词性、依存关系等信息。实验里用的句子比较长输出结果有 18 行每行对应一个词或标点。# 对指定句子做依存句法分析 hanlp parse 徐先生还具体帮助他确定了把画雄鹰、松鼠和麻雀作为主攻目标。是 here-string把后面的字符串作为标准输入传给命令。输出结果里每一行的最后一列是依存关系类型比如“主谓关系”“动宾关系”“核心关系”。核心关系对应的词通常是句子的谓语动词这里是“帮助”。依存句法分析在信息抽取、关系抽取里用得比较多但这份实验只是让你跑通指令没有深入讲怎么用这些关系做下游任务。注意hanlp parse的输出格式在不同版本的 HanLP 里可能有细微差异如果你跑出来的列数和实验报告对不上先确认 pyhanlp 的版本号再对照官方文档调整解析逻辑。3. 词典分词三种匹配算法前向、后向与双向最长匹配的代码实现3.1 三种匹配算法的原理与差异词典分词的核心思路很简单拿一个词典对句子从左到右或从右到左扫描尽可能匹配最长的词。前向最长匹配从左往右扫遇到能匹配的最长词就切出来后向最长匹配从右往左扫逻辑一样但方向相反双向最长匹配则是两边都跑一遍然后按某种规则选一个结果。实验里用“结婚的和尚未结婚的”这个句子做测试三种算法的结果分别是算法分词结果前向最长匹配结婚 / 的 / 和尚 / 未 / 结婚 / 的后向最长匹配结婚 / 的 / 和 / 尚未 / 结婚 / 的双向最长匹配结婚 / 的 / 和 / 尚未 / 结婚 / 的前向匹配把“和尚”切出来了但“尚未”被拆成“未”后向匹配则相反切出了“尚未”但把“和尚”拆开了。双向匹配在这里选了后向的结果因为它的规则通常是如果两种匹配的词数不同选词数少的如果词数相同选单字少的。这个句子里后向结果没有单字所以胜出。3.2 前向与后向最长匹配的 Python 实现实验报告里没有贴出三种匹配算法的完整代码但根据课本第二章的参考实现可以补全如下。核心逻辑是用一个词典集合然后按方向扫描。# 词典实际使用时可以从文件加载 dict_words set([结婚, 的, 和, 和尚, 尚未, 未, 了]) 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): word text[i:ij] if word in dictionary: result.append(word) 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): word text[i-j:i] if word in dictionary: result.append(word) i - j matched True break if not matched: result.append(text[i-1]) i - 1 return result[::-1] # 反转回正常顺序 text 结婚的和尚未结婚的 print(前向:, forward_max_match(text, dict_words)) print(后向:, backward_max_match(text, dict_words))前向匹配的for j in range(min(len(text) - i, 5), 0, -1)这一行是关键它从当前剩余文本的最大可能长度不超过 5开始往下试保证优先匹配长词。后向匹配的逻辑对称只是从右往左扫最后把结果反转回来。参数5是最大词长实际词典里最长的词可能更长这个值要根据词典调整。如果设得太小长词永远匹配不上设得太大循环次数增加但影响不大。3.3 双向最长匹配的选优策略双向匹配不是简单地把前向和后向结果合并而是有一套比较规则。常见做法是先分别跑前向和后向然后统计各自的词数、单字数和总词长按优先级选一个。def bidirectional_max_match(text, dictionary): 双向最长匹配按词数和单字数选优 fwd forward_max_match(text, dictionary) bwd backward_max_match(text, dictionary) # 统计指标词数越少越好单字数越少越好 fwd_single sum(1 for w in fwd if len(w) 1) bwd_single sum(1 for w in bwd if len(w) 1) if len(fwd) ! len(bwd): return fwd if len(fwd) len(bwd) else bwd elif fwd_single ! bwd_single: return fwd if fwd_single bwd_single else bwd else: # 完全打平默认返回前向 return fwd print(双向:, bidirectional_max_match(text, dict_words))这段代码里fwd_single和bwd_single分别统计两种结果里的单字数量。选优逻辑是词数少的优先词数相同则单字少的优先还打平就返回前向。这个规则不是唯一的不同教材可能用不同的 tie-breaking 策略但核心思想都是“尽量减少切分碎片”。实验里双向匹配返回了后向的结果说明后向在词数和单字数上都不差。提示双向匹配并不能保证一定比单向好它只是多了一个选择。在“结婚的和尚未结婚的”这个句子里后向结果确实更符合语义但换一个句子可能前向更好。所以实际工程里词典分词通常只作为基线真正上线还是得靠统计模型。4. 二元语法分词实战从语料训练到 Viterbi 解码4.1 训练语料的格式与模型文件说明二元语法分词的核心是统计词与词之间的转移概率然后用 Viterbi 算法找一条概率最大的切分路径。实验里给了一个极简语料my_cws_corpus.txt只有三行商品 和 服务 商品 和服 物美价廉 服务 和 货币这个语料小到几乎没什么统计意义但用来跑通流程足够了。训练代码调用了 HanLP 的NatureDictionaryMaker它会统计一元词频和二元词频输出三个文件文件名内容作用my_cws_corpus_model.txt单词、词性、词性频次一元语法模型提供词频my_cws_corpus_model.tr.txt词性标注相关词性转移统计my_cws_corpus_model.ngram.txt二元语法模型词与词的转移频次训练代码本身不长但有几个细节容易忽略。CorpusLoader.convert2SentenceList()读入的语料格式是每行一句词之间用空格分隔。word.setLabel(n)把所有词的词性统一设成名词这是为了简化实际训练时应该保留真实词性。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)maker.compute(sents)是核心步骤它遍历所有句子统计每个词的出现次数以及相邻词对的出现次数。saveTxtTo会把统计结果写成文本文件方便你直接打开看。训练完之后my_cws_corpus_model.txt里应该能看到“商品 n 2”这样的行表示“商品”作为名词出现了 2 次。4.2 加载模型并查询词频训练完模型后需要把 HanLP 的默认词典路径指向新模型才能用自定义词频做分词。实验里查了“商品”和“和”的频率结果都是 2。from pyhanlp import * def load_bigram(model_path): # 覆盖 HanLP 的默认词典路径 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)这里的关键是HanLP.Config.CoreDictionaryPath和BiGramDictionaryPath这两个配置项。它们默认指向 HanLP 自带的大词典改成自定义模型路径后后续所有分词操作都会用新词典。CoreDictionary.getTermFrequency()返回的是词频注意这个频次是语料里的原始计数不是概率。如果你要算概率还得除以总词数。注意修改HanLP.Config是全局生效的如果你在同一进程里先用了默认词典再切换到自定义词典可能会出现缓存不一致的问题。稳妥的做法是每次切换后重启 Python 进程或者用HanLP.Config的 reset 方法。4.3 ViterbiSegment 分词与结果解读加载自定义模型后用ViterbiSegment类对“商品和服务”进行分词结果是[商品/n, 和/n, 服务/n]。这个结果和默认词典的分词结果可能不同因为自定义语料里“商品 和 服务”是作为一个句子出现的二元语法模型会倾向于把“和”单独成词。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()ViterbiSegment是 HanLP 里基于 Viterbi 算法的分词器它会在所有可能的切分路径里找概率最大的那条。概率由一元词频和二元转移频次共同决定。在这个极简语料下“商品”和“服务”都是一元词“和”也是而且“商品 和”和“和 服务”在语料里都出现过所以 Viterbi 会倾向于切成三个单字词。如果你把语料换成“商品 和服 物美价廉”那“和服”就会变成一个词分词结果可能变成[商品/n, 和服/n, 物美价廉/n]。这个实验的价值在于让你看到分词结果完全取决于语料。语料里有什么词、词与词的搭配频率如何直接决定了模型怎么切。所以实际做分词项目时领域语料的收集和标注才是重头戏算法本身反而是标准化的。5. 避坑与排查HanLP 实验里最容易翻车的五个地方5.1 编码报错 UnicodeDecodeError现象读取停用词表或语料文件时抛出UnicodeDecodeError: gbk codec cant decode byte 0x90 in position 2519。原因Windows 中文版默认用 GBK 编码打开文件但文件本身是 UTF-8 编码。HanLP 的词典文件和语料文件基本都是 UTF-8所以一读就炸。解决在open()里显式指定encodingutf-8。实验报告里也是这么改的。如果你不确定文件编码可以用chardet检测一下或者直接用二进制模式读入再解码。# 错误写法 with open(stopwords.txt) as f: ... # 正确写法 with open(stopwords.txt, encodingutf-8) as f: ...5.2 pyhanlp 导入失败或 JPype 报错现象import pyhanlp直接报ModuleNotFoundError或者JPype相关的RuntimeError。原因Python 版本太高3.9导致 JPype 不兼容或者 pip 安装时没有正确编译 JPype。解决降级到 Python 3.7 或 3.8。如果已经装了高版本用 conda 新建一个 3.8 环境。另外Windows 上需要安装 Visual C Build Tools否则 JPype 编译会失败。装完依赖后跑python -c import jpype; print(jpype.__version__)确认 JPype 能正常导入。5.3 模型路径配置后分词结果没变化现象改了HanLP.Config.CoreDictionaryPath指向自定义模型但分词结果和默认词典一模一样。原因HanLP 的词典是懒加载的第一次调用分词时才会读入词典并缓存。如果你在修改配置之前已经跑过分词缓存里还是旧词典。解决把配置修改放在所有分词操作之前或者每次修改后重启 Python 进程。更稳妥的做法是用HanLP.Config的reset()方法清空缓存但这个方法在不同版本里行为可能不一致重启进程是最保险的。5.4 训练语料格式错误导致模型为空现象train_bigram跑完没有报错但生成的模型文件是空的或者词频全是 0。原因语料文件里词之间的分隔符不是空格或者每行末尾有不可见字符。CorpusLoader.convert2SentenceList()对格式很敏感它按空格切词如果用了制表符或全角空格切出来的就是整个句子作为一个词。解决用文本编辑器打开语料文件确认词之间是半角空格。可以用cat -A my_cws_corpus.txt查看不可见字符。另外语料文件必须是 UTF-8 编码GBK 编码的文件读进去会乱码。5.5 停用词去除结果里出现连续星号现象用replace_stropwords_text去掉停用词后结果里出现***这样的连续替换符。原因停用词表里有单字停用词比如“的”“和”它们被替换成*后如果相邻多个停用词连续出现就会产生多个星号。解决这是正常现象不是 bug。如果你不想要连续星号可以在替换时判断前一个字符是否已经是替换符或者直接用空字符串替换而不是*。实验里用*是为了让你看清哪些位置被替换了实际工程里通常直接删除停用词。6. 从实验到实战自定义词典与二元语法的进阶用法实验里训练二元语法用的语料只有三行这在实际项目里连玩具都算不上。但如果你把这个流程放大到几千几万行领域语料它就能变成一个可用的分词模型。我一般会按下面的步骤把实验代码改造成能处理真实数据的版本。第一步是准备语料。HanLP 的CorpusLoader要求每行一句、词间空格分隔这个格式和很多标注工具的输出一致。如果你手头的是 PKU 或 MSR 格式的语料需要先写个转换脚本把词性标注去掉或者保留。语料规模建议至少几千句否则统计出来的二元概率太稀疏Viterbi 解码时经常遇到零概率转移。第二步是调整训练参数。NatureDictionaryMaker默认会统计所有词性和转移但如果你只关心分词不关心词性可以在setLabel时统一设成n减少模型体积。另外maker.compute(sents)之后可以调用maker.getDictionary()拿到词典对象手动过滤掉低频词避免模型被噪声词带偏。第三步是验证。训练完模型后不要只看一两个句子的分词结果而是拿一个标注好的测试集跑一遍算准确率和召回率。HanLP 没有内置评测工具但你可以自己写个简单的对比脚本def evaluate(segment, test_sents, gold_sents): 简单的分词评测逐句对比 correct 0 total 0 for sent, gold in zip(test_sents, gold_sents): pred [term.word for term in segment.seg(sent)] gold_words gold.split() # 按词边界对齐比较这里简化成集合比较 correct len(set(pred) set(gold_words)) total len(gold_words) return correct / total if total 0 else 0这个评测函数很粗糙只是按词集合算重叠率但能快速看出模型有没有跑偏。更严谨的做法是按词边界计算 F1 值需要对齐每个词的位置。第四步是处理未登录词。二元语法模型对词典里没有的词只能拆成单字这在人名、地名、新词上表现很差。常见做法是叠加一个 CRF 或 BiLSTM 模型做序列标注或者用 HanLP 的CRFSegment替代ViterbiSegment。实验里用的-a crf参数就是切换到 CRF 模型它在未登录词识别上比 Viterbi 强不少。还有一个容易被忽略的点是词典的更新。HanLP 的CoreDictionary是静态的训练完就固定了。如果你后续有新词加入要么重新训练模型要么用CustomDictionary动态添加。CustomDictionary.add(新词)可以在运行时插入新词但只对词典分词生效对二元语法模型的影响有限。提示自定义词典的路径可以通过HanLP.Config.CustomDictionaryPath配置支持多个词典文件。格式是每行一个词可以带词性和频次比如自然语言处理 n 100。从那以后我每次做分词实验都会先拿一小段语料跑通全流程确认模型文件生成正常、词频统计符合预期再放大到全量数据。这个习惯帮我省了很多次“训练半小时、结果全为零”的后悔药。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号