从词典匹配到二元语法:手把手入门中文分词与NLP基础
简介:这份资源是南京邮电大学自然语言处理课程实验一的完整实验报告,面向正在学习中文分词与句法分析的高校学生及自学者,帮助其理解词典分词与二元语法分词的基本原理并完成实验任务。报告围绕HanLP工具展开,涵盖segment分词指令与--no-tag参数用法、文件输入输出处理、parse句法分析及依存关系解读,并给出前向最长匹配、后向最长匹配与双向最长匹配三种算法的分词结果对比和差异说明,还附有基于pyhanlp的Python实现代码。资源包共1个doc文件,约232KB,以实验报告文档形式呈现,结构贴合课程实验流程,便于对照填写与复盘。已有428人学习下载,适合需要参考实验步骤、核对分词结果或补充代码实现的学习者使用。
1. 分词这件事,其实比你想的更接地气
你有没有想过,手机输入法为什么能猜出你下一句想打什么?搜索引擎为什么能在一大段话里找到你要的那个词?这背后有一个最基础、也最容易被忽略的步骤——把一整句话切成一个个有意义的词。中文里这叫“分词”,英文里叫 word segmentation。听起来像学术黑话,但你每天都在用它。
自然语言处理(NLP)入门的第一课,几乎都是从分词开始的。原因很简单:中文不像英文有空格隔开单词,“结婚的和尚未结婚的”这句话,机器看到的就是一串连续的汉字,它得先决定怎么切,才能继续往下理解。这份实验资源就是带你从零手搓这件事:用词典匹配的方法切词,再用二元语法(bigram)统计的方法切词,最后跑通一个能实际输出结果的例子。适合谁?刚接触 NLP、装了 Python 但不知道从哪下手、看到“语料库”“词性标注”就发怵的新手。读完你至少能自己跑出第一行分词结果。
2. 先搞懂两个概念:词典分词和二元语法到底在干嘛
2.1 词典分词:拿一本“词表”去句子里对号入座
想象你手里有一本厚厚的词典,里面列着所有你认为算“词”的字符串。现在给你一句话,你从第一个字开始,看最多能匹配到词典里多长的词,匹配上了就切出来,然后从下一个字继续。这就是最朴素的词典分词,也叫最长匹配法。
它分三种常见玩法:
- 前向最长匹配:从左往右扫,每次尽量取最长的词。
- 后向最长匹配:从右往左扫,同样尽量取最长。
- 双向最长匹配:两种都跑一遍,按规则挑一个更靠谱的结果。
拿“结婚的和尚未结婚的”举例,前向匹配可能切出['结婚', '的', '和尚', '未', '结婚', '的'],把“和尚未”切成了“和尚 + 未”;后向匹配则切出['结婚', '的', '和', '尚未', '结婚', '的'],明显更符合语义。双向匹配会综合两者,降低这种翻车概率。你看,词典分词不难,难的是歧义——同一串字,不同切法意思完全不一样。
2.2 二元语法分词:不查词典,靠“谁跟谁常一起出现”来切
词典分词依赖一本现成的词表,词表里没有的新词就抓瞎。二元语法分词换了个思路:我不告诉你哪些是词,我给你一堆已经分好词的句子(叫语料),你统计一下相邻两个字/词一起出现的频率。切分的时候,哪种切法让相邻词对的出现概率最高,就选哪种。
比如语料里有“商品 和 服务”“商品 和服 物美价廉”“服务 和 货币”,统计完你会发现“商品”后面跟“和”的次数是 2,“和”后面跟“服务”的次数也是 2。那给“商品和服务”分词时,商品 / 和 / 服务这条路径的联合概率就比商品 / 和服 / 务更靠谱。这就是统计语言模型的入门版:不靠规则,靠数据说话。
2.3 为什么新手要先学这两个,而不是直接上深度学习
我第一次学 NLP 的时候也想过:现在都有大模型了,还学词典匹配干嘛?后来踩了坑才明白,词典分词和二元语法是理解所有后续模型的“地基”。你后面遇到的隐马尔可夫、条件随机场、甚至 BERT 的分词器,底层逻辑都绕不开“怎么切、为什么这样切”。而且这两个方法代码量极小,跑通只要几十行,能让你在第一天就获得“我确实让机器处理了中文”的正反馈。先建立手感,再谈原理,这是最不容易放弃的路径。
3. 动手跑第一个例子:从装环境到出结果
3.1 环境准备:Python 加一个中文处理库
这份实验用的是 HanLP 这个中文处理工具包,Python 3.7 或 3.8 都能跑。你不需要理解它内部怎么实现,先把它当成一个“分词黑匣子”用起来。打开命令行,装好依赖:
pip install pyhanlp装完之后,第一次运行会自动下载词典数据,稍微等一会儿。如果你在 Windows 上遇到编码报错,别慌,后面避坑章节会讲。验证是否装好,跑一行:
from pyhanlp import HanLP # 最简单的分词调用,输出带词性的结果 print(HanLP.segment("商品和服务"))这段代码的意思很直白:把“商品和服务”交给 HanLP 的默认分词器,打印结果。你会看到类似[商品/n, 和/cc, 服务/vn]的输出,斜杠后面是词性标注(n 是名词,cc 是连词,vn 是动名词)。如果你只想要词、不想要词性,后面加参数或者用--no-tag的思路处理即可。
3.2 用命令行快速试几句,建立直觉
HanLP 装好后自带命令行入口,适合你快速试不同句子、观察切分差异:
hanlp segment进入交互模式后,依次输入这几句,看看输出:
商品和服务 当下雨天地面积水分外严重 结婚的和尚未结婚的确实在干扰分词啊第一句你会看到商品/n, 和/cc, 服务/vn;第二句是当/p, 下雨天/n, 地面/n, 积水/n, 分外/d, 严重/a;第三句里“和尚”和“尚未”的歧义会被正确处理成结婚/vi, 的/ude1, 和/cc, 尚未/d, 结婚/vi...。这一步的目的不是背结果,而是让你亲眼看到:同一套工具,对不同歧义句的处理策略是不一样的。你输入得越多,对“分词难在哪”的体感就越强。
3.3 第一个完整脚本:文件进、文件出
命令行玩熟了,写一个能批量处理文件的脚本,才算真正迈进门。假设你有一个input1.txt,里面是一段中文,你想分词后写到output1.txt:
from pyhanlp import HanLP # 读入原始文本 with open("input1.txt", encoding="utf-8") as f: text = f.read() # 分词,ShowTermNature 设为 False 表示不显示词性 HanLP.Config.ShowTermNature = False words = HanLP.segment(text) # 把结果拼成空格分隔的字符串,写入输出文件 result = " ".join(str(w) for w in words) with open("output1.txt", "w", encoding="utf-8") as f: f.write(result) print("分词完成,共切出", len(words), "个词")逐行解释一下:encoding="utf-8"是防止中文乱码的关键,很多新手第一次翻车就翻在这里;ShowTermNature = False控制输出里要不要带/n这种词性标记;" ".join(...)把词列表拼成一行,方便你肉眼检查。跑完打开output1.txt,你应该能看到类似春分 最 具 灵性 的 美 , 是 千 色 万 彩 ...的结果。到这一步,你已经完成了“读文件 → 分词 → 写文件”的完整闭环,后面所有实验都是在这个骨架上加东西。
4. 再进一步:自己训练一个二元语法模型
4.1 准备一份迷你语料,亲手喂给模型
词典分词你已经在用了,现在试试二元语法。新建一个文件my_cws_corpus.txt,内容就四行,词与词之间用空格隔开:
商品 和 服务 商品 和服 物美价廉 服务 和 货币这四行就是你的“训练数据”。别嫌少,入门阶段数据越少,你越容易看清每一步在干什么。接下来用 HanLP 的语料加载和词典构建工具,把这份语料训练成一个模型:
from pyhanlp import SafeJClass # 加载语料,转成句子列表 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) # 给每个词统一打上词性标签 n,入门阶段不用纠结词性 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.ngram.txt是二元模型(相邻词对出现了几次),还有一个.tr.txt跟词性标注有关,入门阶段可以先不管。这三个文件就是你的“小型语言知识库”。
4.2 加载模型,查词频、跑分词
模型训练好了,怎么用?先查两个词的频率,验证模型确实学到了东西:
from pyhanlp import HanLP, SafeJClass 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")输出应该是商品: 2和和: 2,因为你的语料里“商品”出现两次、“和”也出现两次。这说明模型文件被正确加载了。接着用这个模型对“商品和服务”做分词:
from pyhanlp import HanLP, JClass 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() print(segment.seg("商品和服务"))结果会是[商品/n, 和/n, 服务/n]。注意这里的“和”被标成了 n,因为训练时我们统一打了 n 标签,这不影响分词本身。你可能会问:这跟默认分词有什么区别?区别在于,默认分词用的是 HanLP 自带的大词典,而这里用的是你四行语料训出来的小模型。数据少,效果自然糙,但流程是完整的——你亲手走了一遍“语料 → 统计 → 模型 → 分词”的链路,这比背十遍概念都管用。
5. 新手最容易踩的五个坑
5.1 一跑就报 UnicodeDecodeError
现象:打开文件时抛出UnicodeDecodeError: 'gbk' codec can't decode byte...。原因:Windows 默认用 GBK 编码读文件,而你的文本是 UTF-8 存的,编码对不上。解决:所有open()都显式写上encoding="utf-8",包括读和写。这是血泪经验,我第一次跑实验卡了半小时就因为这个。
5.2 分词结果里全是/null
现象:用DoubleArrayTrieSegment输出类似[春分/null, 之/null, ...]。原因:这个分词器只做词典匹配,不负责词性标注,所以词性位置是 null。解决:这是正常现象,不是代码错了。如果你只关心切分结果,忽略/null即可;想要词性就换回HanLP.segment()。
5.3 自己训练的模型分词效果“很傻”
现象:用四行语料训出来的模型切“春分之特别”,结果惨不忍睹。原因:语料太小,模型没见过那些词,统计概率全是零。解决:入门阶段接受这个结果,它的目的是让你理解流程,不是追求效果。想提升就加语料,但那是下一步的事。
5.4 命令行hanlp segment找不到命令
现象:终端提示hanlp 不是内部或外部命令。原因:pyhanlp装在了某个 Python 环境里,但该环境的 Scripts 目录没加进系统 PATH。解决:直接用 Python 脚本调用HanLP.segment(),绕开命令行入口;或者确认你激活的是装包的那个环境。
5.5 停用词去除后标点全变星号
现象:去掉停用词后,逗号句号也被替换成了*。原因:停用词表里包含了标点符号,替换逻辑一视同仁。解决:这是预期行为,不是 bug。如果你只想去虚词、保留标点,需要自己过滤停用词表,把标点剔出去。入门阶段先接受原样输出,理解机制比调结果重要。
6. 下一步学什么:把这条链路走宽
跑通分词和二元语法之后,你其实已经站在 NLP 的门口了。接下来怎么走,我给你一条我当初希望有人告诉我的路径。
第一周,把分词吃透。别急着换模型,先把前向、后向、双向最长匹配用纯 Python 各写一遍,不用 HanLP,就用手写循环加一个set当词典。写完你会对“歧义”有肌肉记忆。然后拿同一句话分别跑三种匹配,对比结果差异,记录哪种情况谁翻车。
第二周,理解词性标注和句法分析在干嘛。实验里出现的hanlp parse输出那一堆“主谓关系”“动宾关系”,不用深究算法,但你要能看懂一棵依存树大概在描述什么。找一个简单句,手动画出词与词的关系,再对照工具输出,看看差在哪。
第三周,换一个分词器对比。HanLP 自带多种分词模式,试试StandardTokenizer和NLPTokenizer对同一段新闻的切分差异。重点不是谁更准,而是观察“为什么同一句话不同工具切得不一样”,这会逼你去想模型假设的差别。
第四周,碰一碰序列标注。二元语法本质是统计相邻词对,而现代分词主流是序列标注(给每个字打 B/M/E/S 标签)。你可以先不写代码,找一篇讲 BMES 标注的入门文章读一遍,再回头看你的二元语法模型,会发现思路是一脉相承的:都是从数据里学“怎么切”。
一个具体技巧:每次跑新代码前,先用三五个短句做“冒烟测试”,确认输出格式对了再上长文本。我早期图省事直接拿整篇文章跑,结果一个编码错误埋在几百行输出里,排查了半天。从那以后我每次换模型、换文件,都强制先用一句“商品和服务”走一遍全流程,确认管道通了再放量。这个习惯帮我省下的时间,比任何调参技巧都多。
希望帮到你,先从第一行HanLP.segment("商品和服务")跑起来,剩下的路会越走越顺。
本文还有配套的精品资源,点击获取