【Datawhale学习笔记】NLP初级分词技术

举报
JeffDing 发表于 2026/01/08 10:43:07 2026/01/08
【摘要】 分词的定义与重要性 分词的任务是把连续的文本序列切分成具有独立语义的基本单元(即“词”或“词元”)。对于英文等天然有空格作为分隔符的语言,分词相对简单。但对于中文、日文、泰文等语言,文本是连续的字符流,词之间没有明确的边界。例如,“给阿姨倒一杯卡布奇诺”,计算机需要依据算法将其正确地切分为 ["给", "阿姨", "倒", "一杯", "卡布基诺"]。在传统的 NLP 处理流程中,分词是后...

分词的定义与重要性

分词的任务

是把连续的文本序列切分成具有独立语义的基本单元(即“词”或“词元”)。

  • 对于英文等天然有空格作为分隔符的语言,分词相对简单。
  • 但对于中文、日文、泰文等语言,文本是连续的字符流,词之间没有明确的边界。例如,“给阿姨倒一杯卡布奇诺”,计算机需要依据算法将其正确地切分为 ["给", "阿姨", "倒", "一杯", "卡布基诺"]

在传统的 NLP 处理流程中,分词是后续所有任务的基础。其处理方式通常是将分词作为一个独立且“硬性”的预处理步骤,这就导致一个微小的分词错误就可能造成语义信息的丢失。这种错误会在后续的处理链条中被不断放大,产生“差之毫厘,谬以千里”的级联效应(Cascading Effect)。例如,在传统的搜索引擎里,一旦“南京市长江大桥”被错分为 ["南京", "市长", "江大桥"],系统就很难再从这三个错误的词块中还原出原始的、正确的地理位置含义,导致搜索结果完全跑偏。现代的 NLP 方法则通过更灵活的切分策略,在很大程度上缓解了这个问题。

通过 jieba 认识分词

基于规则与词典

这是最早期也是最符合直觉的分词方法,它的核心是一部大型词典和一套匹配规则。jieba 的默认模式实现的就是这种方式。它首先基于一个前缀词典(Trie树),高效地构建出一个包含句子中所有可能词语组合的有向无环图(DAG)。接着,通过动态规划算法寻找一条概率最大的路径,作为最终分词结果。

这个过程可以被量化为一个概率计算问题。假设一个分词路径由一个词语序列组成,将其表示为 w1,w2,...,wnw_1, w_2, ..., w_n,其中 wiw_i 代表序列中的第 i 个词。那么这条路径的概率可以近似为:

P(w1,w2,...,wn)P(w1)×P(w2)×...×P(wn)P(w_1, w_2, ..., w_n) \approx P(w_1) \times P(w_2) \times ... \times P(w_n)

其中,每个词 wiw_i 的概率 P(wi)P(w_i) 可以通过其在词典(语料库)中的频率来估算:

P(wi)=词 wi 的词频词典中所有词的总词频P(w_i) = \frac{\text{词 } w_i \text{ 的词频}}{\text{词典中所有词的总词频}}

jieba的目标就是找到一条路径,使得这个累乘的概率值最大。

Log概率与动态规划

在实际工程中,将大量小于1的概率值直接相乘,很容易导致结果趋近于0,造成浮点数下溢,从而无法比较路径优劣。

jieba 采用了两种关键技术来解决这个问题:

(1)对数概率:利用对数函数 log 的性质,将概率的累乘转换为 log 概率的累加。寻找概率最大值就等价于寻找 log 概率之和的最大值,有效避免了下溢问题。

argmaxwi=1nlogP(wi)\underset{w}{\arg\max} \sum_{i=1}^{n} \log P(w_i)

(2)动态规划:暴力计算所有可能路径概率和的计算量是很大的。jieba 使用动态规划的思想,从句子的末尾开始,从后向前递推计算到每个位置的最优切分路径及其 log 概率之和,并记录下来。最终,从句子开头出发,根据记录好的最优路径信息,就能反推出整个句子的最优分词结果。

对于句子“给阿姨倒一杯卡布奇诺”,在构建好 DAG 之后,jieba 在“一/一杯”这个位置附近,可能会存在两条候选路径:

路径 A :给 阿姨 倒 一杯 卡布奇诺
路径 B :给 阿姨 倒 一 杯 卡布奇诺
如果直接从“概率乘积”的角度看,两条路径的概率近似为:

P(A)P()×P(阿姨)×P()×P(一杯)×P(卡布奇诺)P(A) \approx P(\text{给}) \times P(\text{阿姨}) \times P(\text{倒}) \times P(\text{一杯}) \times P(\text{卡布奇诺})

P(B)P()×P(阿姨)×P()×P()×P()×P(卡布奇诺)P(B) \approx P(\text{给}) \times P(\text{阿姨}) \times P(\text{倒}) \times P(\text{一}) \times P(\text{杯}) \times P(\text{卡布奇诺})

在真实实现中,jieba 会把上面的乘积全部换成log 概率的加和 来计算:

logP(A)logP()+logP(阿姨)+logP()+logP(一杯)+logP(卡布奇诺)\log P(A) \approx \log P(\text{给}) + \log P(\text{阿姨}) + \log P(\text{倒}) + \log P(\text{一杯}) + \log P(\text{卡布奇诺})

logP(B)logP()+logP(阿姨)+logP()+logP()+logP()+logP(卡布奇诺)\log P(B) \approx \log P(\text{给}) + \log P(\text{阿姨}) + \log P(\text{倒}) + \log P(\text{一}) + \log P(\text{杯}) + \log P(\text{卡布奇诺})

由于对数函数是单调递增的,谁的 log 概率和更大,谁对应的原始概率也更大。 假设“一杯”是一个高频词,它单独出现的概率 P(一杯)P(\text{一杯}) 会远大于两个单字“一”和“杯”的概率之积 P()×P()P(\text{一}) \times P(\text{杯}),于是就有:

logP(一杯)logP()+logP()\log P(\text{一杯}) \gg \log P(\text{一}) + \log P(\text{杯})

因此:

logP(A)>logP(B)\log P(A) > \log P(B)

动态规划要做的,就是**在句子的每一个位置上,自动完成这种“在所有可能后续切分中,挑出 log 概率和最大的那条路径”的比较,而不是像例子里只人工枚举两条路径。最终,它就会把路径 A 选为“全句最优分词结果”。

动手实践

jieba 是目前流行的 Python 中文分词库之一,对初学者较为友好。可以使用如下 pip 命令安装。

jiba的安装

pip install jieba

jiab实践案例

jieba的精确模式正是前文所述 “基于词典和动态规划寻找最大概率路径” 这一方法的典型应用,它会力图将句子尽可能精确地切开。
示例代码

import jieba

text = "我在梦里收到清华大学录取通知书"
seg_list = jieba.lcut(text, cut_all=False) # cut_all=False 表示精确模式
print(seg_list)

程序输出

['我', '在', '梦里', '收到', '清华大学', '录取', '通知书']

下面创建一个自定义字典user_dict.txt:

九头虫
奔波儿灞

体验加载与不加载的区别

# 未加载词典前的错误分词
text = "九头虫让奔波儿灞把唐僧师徒除掉"
print(f"精准模式: {jieba.lcut(text, cut_all=False)}")

# 加载自定义词典
jieba.load_userdict("./user_dict.txt") 
print(f"加载词典后: {jieba.lcut(text, cut_all=False)}")

输出结果

精准模式: ['九头', '虫', '让', '奔波', '儿', '灞', '把', '唐僧', '师徒', '除掉']
加载词典后: ['九头虫', '让', '奔波儿灞', '把', '唐僧', '师徒', '除掉']

精确模式工作流程

jieba 精确模式的分词过程

  • 文本预处理与分块 (cut 方法):cut 函数是总调度。它首先通过正则表达式 re_han_default 将整个句子切分成连续的汉字区块和非汉字部分(如英文、数字、标点)。非汉字部分被直接输出,而每个汉字区块则被送入核心分词流程。
  • 构建有向无环图(DAG) (get_DAG 方法):这一步为每个汉字区块生成一个记录所有可能分词路径的图。
  • 计算最优路径 (calc 方法):这是动态规划算法的核心实现,用于从DAG中寻找最优路径。calc 的计算方向很重要:它是从句子的末尾向前反向计算的 (for idx in xrange(N - 1, -1, -1))
  • 从路由表中重建结果 (__cut_DAG_NO_HMM 方法):当 calc 计算完成后,route 字典中已经储存了从每个位置出发的最优选择。

get_DAG 的逻辑

  • 从句子的第 k 个字开始,向后扫描,形成词语 frag(fragment)
  • 只要 frag 存在于 self.FREQ 这个前缀词典(由主词典与用户词典加载后生成)中,就继续向后扫描。
  • 在扫描过程中,如果 self.FREQ[frag] 的值 不为0 ,说明 frag 是一个能独立成词的词语,就将它的结束位置 i 记录下来。词频为 0 的词条仅被当作前缀,不会被记录为成词路径。
  • 最终,get_DAG 返回一个字典 DAG。

calc计算方法

  • 对于句中的每个位置 idx,它会考察所有从 idx 出发的可能词语(由 DAG[idx] 提供)。
  • 对于每个可能的词 sentence[idx:x + 1],它会计算一个路径“分数”,这个分数由两部分相加而成:
  1. 当前词的log概率:源码中为 log(self.FREQ.get(...) or 1) - logtotal
  2. 该词之后剩余句子的最优log概率:这部分已经在之前的迭代中计算好并储存在 route[x + 1][0] 中。
  • calc 会选择使这个总分数最高的那个词语作为从 idx 出发的“最佳下一步”,并将这个最高分和“下一步”的起始位置 x 记录在 route[idx] 中。

__cut_DAG_NO_HMM 的工作

  • 它从句子的第 0 位开始 (x=0)。
  • 通过 route[x][1] 查找下一步应该跳到哪里,从而得到第一个最优的词。
  • 然后将 x 更新为这个词的结束位置,继续循环查找,直到句子末尾。
  • 最终,通过 yield 逐个返回最优路径上的所有词语。

统计学习时代的方法

为了解决对人工词典的过度依赖,研究者们转向了统计学习。其核心思想是把分词看作一个序列标注问题。它会为每个字标注其在词中的位置(B-Begin, M-Middle, E-End, S-Single),然后利用隐马尔可夫模型(HMM) 等模型来预测每个字最可能的位置标签序列。也就是为句子中的每个字打上一个位置标签,例如:

  • B(Begin):词的开始
  • M(Middle):词的中间
  • E(End):词的结束
  • S(Single):单字成词
    这样,“我爱北京”就会被标注为 S S B E。分词任务就变成了为字序列寻找最合理的标签序列。

Jieba 实践:HMM对未登录词的识别

直观地展示了 __cut_DAG 中HMM模块

text = "我在Boss直聘找工作"

# 开启HMM(默认)
seg_list_hmm = jieba.lcut(text, HMM=True)
print(f"HMM开启: {seg_list_hmm}")

# 关闭HMM
seg_list_no_hmm = jieba.lcut(text, HMM=False)
print(f"HMM关闭: {seg_list_no_hmm}")

输出结果

HMM开启: ['我', '在', 'Boss', '直聘', '找', '工作']
HMM关闭: ['我', '在', 'Boss', '直', '聘', '找', '工作']

词性标注

除了分词,jieba 还提供了词性标注功能。采用了词典查询与隐马尔可夫模型相结合的混合策略,来识别出每个词语的语法属性(名词、动词、形容词等)。这需要使用jieba.posseg模块。

import jieba.posseg as pseg

text = "九头虫让奔波儿灞把唐僧师徒除掉"

# HMM=False 强制只使用词典和动态规划
words = pseg.lcut(text, HMM=False)
print(f"默认词性输出: {words}")

输出结果

默认词性输出: [pair('九头虫', 'x'), pair('让', 'v'), pair('奔波儿灞', 'x'), pair('把', 'p'), pair('唐僧', 'nr'), pair('师徒', 'n'), pair('除掉', 'v')]

由于在前面已经通过 jieba.load_userdict() 加载了包含“奔波儿灞”的词典,因此 jieba 已经能够正确地将其切分出来。但是,因为初始词典未提供词性,jieba 会给它一个默认的、不一定准确的词性(如下面的x)。

import jieba.posseg as pseg

text = "九头虫让奔波儿灞把唐僧师徒除掉"

# HMM=False 强制只使用词典和动态规划
words = pseg.lcut(text, HMM=False)
print(f"默认词性输出: {words}")

此时的输出

默认词性输出: [pair('九头虫', 'x'), pair('让', 'v'), pair('奔波儿灞', 'x'), pair('把', 'p'), pair('唐僧', 'nr'), pair('师徒', 'n'), pair('除掉', 'v')]

分词是正确的,但“九头虫”和“奔波儿灞”的词性是x(非语素字),接下来,尝试通过调整词频来“干预”分词路径。如果希望将‘九头虫’完全切分为‘九’、‘头’、‘虫’三个单字,可以修改词典如下。为单个字“九”和“头”赋予了很高的词频,这会使jieba在进行动态规划计算时,认为 九+头 这条路径的概率远大于 九头 这条路径,从而优先选择前者。

将user_pos_dict.txt修改为:

10000000 n
头 1000000 n
奔波儿灞 nr

为了让修改生效,需要重新加载词典

# 重新加载修改后的词典
jieba.load_userdict("./user_pos_dict.txt")

dic_words = pseg.lcut(text, HMM=False)
print(f"加载词性词典后: {dic_words}")

输出结果

加载词性词典后: [pair('九', 'n'), pair('头', 'n'), pair('虫', 'n'), pair('让', 'v'), pair('奔波儿灞', 'nr'), pair('把', 'p'), pair('唐僧', 'nr'), pair('师徒', 'n'), pair('除掉', 'v')]

从“分词”到“分块”

字粒度分词(Character-level)

以 BERT 模型为代表,在处理中文时,其最基础的分词策略就是字粒度,即直接将每个汉字视为一个独立的Token

  • 优点
  1. 有效解决了 OOV 问题:常用汉字的数量是有限的(几千个),模型可以构建一个全覆盖的“字表”。任何由标准汉字组成的词语都不会“未登录”,因为构成它的每个字都在字表里。
  2. 无需维护庞大词典:摆脱了对词典的依赖。
  • 缺点
  1. 丢失词汇语义:像“博物馆”这样的词,其整体语义在输入层面被人为地拆散为三个独立的字,模型需要消耗更多的计算资源在内部重新学习这些字的组合关系。
  2. 输入序列更长:相较于词,字的序列长度会显著增加,加大了模型的处理负担。

子词分词(Subword)

以 GPT 系列为代表的大语言模型,则采用了更灵活的子词(Subword) 切分方案,其中最主流的算法是 BPE(BytePair Encoding) 3。
BPE的核心思想是:在原始的字符语料库上,迭代地将高频的相邻字节对(或字符对)合并成一个新的、更大的单元,并将其加入词表。
例如,对于语料中频繁出现的 “deeper”,BPE可能会先学习到 “er”,然后是 “deep”,最终可能将 “deeper” 作为一个整体或"deep"+“er” 的组合加入词表。

  • 优点
  1. 有效平衡 :它在“词”和“字”之间取得了较好的平衡。高频词(如“机器学习”)可以被完整保留,低频词(如“擘画”)可以被拆分为更小的有意义的子词或单字(擘+画),而OOV(如一个新造的网络词)则可以被拆解成字符或字节组合,从而在保持信息完整性的前提下有效解决了OOV问题。
  2. 词表大小可控:可以通过控制合并次数,将词表大小有效控制在预设范围内。
  • 缺点 :
  1. 语言学可解释性不强:BPE等算法产生的子词通常是基于统计频率,而非语言学上的词根、词缀等有意义的语素。这使得分词结果的可解释性变差。
  2. 对训练语料的强依赖性:子词词表是根据特定语料训练的,对于领域外的文本,其分词效果可能会退化,将很多词切成单字,导致处理效率降低。

番外篇:实践

尝试修改 user_pos_dict.txt 中九和头的词频看看结果有什么不同

修改后的user_pos_dict.txt

100000 n
头 1000 n
奔波儿灞 nr

运行结果

加载词性词典后: [pair('九头虫', 'x'), pair('让', 'v'), pair('奔波儿灞', 'nr'), pair('把', 'p'), pair('唐僧', 'nr'), pair('师徒', 'n'), pair('除掉', 'v')]

自行更换代码中使用的 text 短句看看会得到什么输出

text更换为上海是一个美丽的城市后的结果

import jieba.posseg as pseg

text = "上海是一个美丽的城市"

# HMM=False 强制只使用词典和动态规划
words = pseg.lcut(text, HMM=False)
print(f"默认词性输出: {words}")

输出结果

默认词性输出: [pair('上海', 'ns'), pair('是', 'v'), pair('一个', 'm'), pair('美丽', 'ns'), pair('的', 'uj'), pair('城市', 'ns')]

修改user_pos_dict.txt

100000000 n
海 100000 n
上海 nr

加载修改后的词典结果

# 重新加载修改后的词典
jieba.load_userdict("./user_pos_dict.txt")

dic_words = pseg.lcut(text, HMM=False)
print(f"加载词性词典后: {dic_words}")

输出结果

加载词性词典后: [pair('九头虫', 'x'), pair('让', 'v'), pair('奔波儿灞', 'nr'), pair('把', 'p'), pair('唐僧', 'nr'), pair('师徒', 'n'), pair('除掉', 'v')]

参考资料

https://github.com/datawhalechina/base-nlp/blob/main/docs/chapter2/03_tokenization.md

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。