LLM系列:4.transformer算法:3.Jieba

Jieba

一.认识Jieba

Jieba 是目前表现较为不错的、立志于做最好的 Python 中文分词组件。 它不仅支持多种分词模式,还支持自定义词典和繁体分词,且采用 MIT 授权协议。

安装与导入 Jieba:可以直接通过 pip 进行安装,支持 Python 2/3。

python 复制代码
# 安装
pip3 install jieba 

# 导入
import jieba 

二.Jieba的分词原理与步骤

jieba分词综合了基于字符串匹配的算法和基于统计的算法。 其主要底层算法及分词步骤如下:

  • 初始化与加载: 加载词典文件,获取每个词语和它出现的词数。
  • 切分短语: 利用正则,将文本切分为一个个语句,之后对语句进行分词。
  • 构建DAG (有向无环图): 基于前缀词典实现高效的词图扫描,通过字符串匹配,构建所有可能的分词情况的有向无环图 (DAG)。
  • 计算最大概率路径: 采用了动态规划查找最大概率路径,找出基于词频的最大切分组合;同时计算每个汉字节点到语句结尾的所有路径中的最大概率,并记下对应位置。
  • 构建切分组合: 根据节点路径,得到最终的词语切分结果。
  • HMM新词处理: 对于未登录词 (dict.txt中没有的词语),采用基于汉字成词能力的 HMM (隐马尔科夫) 模型来处理,并使用了 Viterbi 算法。
  • 返回结果: 通过 yield 将切分好的词语逐个返回,相较于直接返回 list,这可以节约存储空间。

三.Jieba 的核心类与属性

jieba 的底层设计中,大多数直接调用的模块级方法(如 jieba.cut)实际上是代理给了默认实例化的核心类。

1. 核心类 (Classes)
  • jieba.Tokenizer : 这是 jieba 的核心分词器类。每次 import jieba 时,模块会自动创建一个默认的 Tokenizer 实例(即 jieba.dt)。如果你在多线程环境或需要隔离的词典环境,可以自行实例化这个类(例如 my_tokenizer = jieba.Tokenizer(dictionary="..."))。
  • jieba.posseg.POSTokenizer: 专门用于词性标注的分词器类,继承/组合了基础的分词能力并集成了词性标注模型。
  • jieba.analyse.TFIDF / jieba.analyse.TextRank: 用于关键词提取的评估类。
2. 核心属性 (Attributes)

通常不建议直接操作底层属性,但了解它们有助于理解原理:

  • jieba.dt : 默认的 Tokenizer 实例,所有全局方法(jieba.cut 等)都在操作这个实例。
  • jieba.dt.FREQ: 一个 Python 字典,存储了加载的词库中所有词语及其对应的词频(包括前缀词)。
  • jieba.dt.total: 统计出的所有词汇的总词频(用于计算概率分布)。
  • jieba.dt.initialized: 布尔值,标识词典是否已经被加载和初始化。

四.Jieba 的分词模式

Jieba 库支持以下四种主要的分词模式:

分词模式 描述与特点
精确模式 试图将文本精确地切分成若干个中文单词,最精确地还原为之前的文本。适合文本分析,其中不存在冗余单词。
全模式 将一段文本中所有可能的、可以成词的词语都扫描出来。速度非常快,但不能解决歧义,分词后的信息组合起来会有冗余。
搜索引擎模式 在精确模式的基础上,对发现的长词再次进行切分,从而提高召回率。适合用于搜索引擎分词和索引,同样会存在冗余。
paddle模式 通过延迟加载方式,在安装 paddlepaddle-tiny 后提供的模式。

1. 精确模式 (Precise Mode)

这是 Jieba 的默认模式,也是日常开发中最常用的模式。

  • 核心机制:它会结合基于词典的 DAG(有向无环图)最大概率路径算法,以及基于 HMM(隐马尔可夫模型)的 Viterbi 算法来处理未登录词。系统会力求根据上下文找到一条唯一且最合理的切分路径。
  • 特点:毫无冗余,句子中的每个字只会属于一个词,最终组合起来能完美还原原文。
  • 适用场景:绝大多数常规的自然语言处理(NLP)任务,如文本特征提取、情感分析、文本分类、词云生成等,因为它能最大程度保留句子的正常语义。
  • 调用方式
python 复制代码
jieba.cut(text, cut_all=False)  # 默认就是 False

2. 全模式 (Full Mode)

顾名思义,这是一种"地毯式搜索"的暴力模式。

  • 核心机制 :主要依赖纯词典匹配,把句子中从每一个位置开始,只要能和词典匹配上的词全部扫描出来。它不考虑上下文语境 ,也不会开启 HMM 模型去猜测新词。
  • 特点:切分速度极快(因为跳过了复杂的概率计算),但分词结果会有大量的重叠和冗余,完全不具备解决词义歧义的能力。
  • 适用场景:对处理速度要求极高,且需要极高"召回率"(宁可错杀一千,不可放过一个)的粗粒度文本扫描任务。通常不适合直接用于语义分析。
  • 调用方式
python 复制代码
jieba.cut(text, cut_all=True)

3. 搜索引擎模式 (Search Engine Mode)

这是在"精确模式"基础上做加法的一种模式,专门为了解决搜索场景痛点而生。

  • 核心机制 :属于"二次加工"。它首先在底层执行一次精确模式,然后对精确切分出来的长词(通常指长度大于 2 的词)进行再次拆解,提取出其中包含的更短的词汇。
  • 特点:在保留基础语义环境的同时,增加了细粒度词汇的输出。
  • 适用场景 :专门用于构建搜索引擎的倒排索引(如配合 Elasticsearch 等使用)。在搜索场景下,用户经常只输入长词的一部分(比如原文是"计算机科学",用户可能只搜"计算"),这种模式能确保长短词汇都能被索引到,显著提升搜索命中率。
  • 调用方式
python 复制代码
jieba.cut_for_search(text)

4. Paddle模式 (Paddle Mode)

引入了深度学习框架,是 Jieba 拥抱神经网络架构的一种扩展模式。

  • 核心机制:脱离了传统的词典统计与概率推算,底层直接调用了百度飞桨(PaddlePaddle)预训练的深度学习序列标注模型(通常基于 BiGRU+CRF 架构)。
  • 特点:在处理长难句、解决复杂语境歧义、以及发现未登录词(新词、专有名词、网络梗)方面表现远超基础模式,且可以同时输出高精度的词性标注。但缺点是依赖较重,初次启动和运行速度比传统词典法慢,资源消耗更大。
  • 适用场景:对分词准确率要求极高、语料库包含大量非常规或新生词汇、且硬件资源允许的深度 AI 分析场景。
  • 调用方式
python 复制代码
# 必须先安装 paddlepaddle-tiny
jieba.enable_paddle() # 开启并加载模型
jieba.cut(text, use_paddle=True)

五.基础分词方法

1. cut & lcut - 精确与全模式分词

作用:对目标文本进行精确模式或全模式的切分。cut 返回一个可迭代的生成器,能够在处理长文本时节约内存;而 lcut 则直接将全部分词结果转化为列表返回。

python 复制代码
jieba.cut(sentence, cut_all=False, HMM=True)
jieba.lcut(sentence, cut_all=False, HMM=True)

参数:

  • 文本 (sentence): 需要分词的目标中文字符串(str)。
  • 全模式 (cut_all): 控制分词模式。设为 False 为精确模式(默认),设为 True 则为全模式(bool)。
  • 隐马尔可夫模型 (HMM): 是否开启HMM模型来识别未登录词(新词)。默认为 True(bool)。
    • 关闭HMM(HMM=False),Jieba 遇到不认识的词时,就会极其生硬地把它们全部切碎成单独的汉字。
    • 开启HMM(HMM=True)后,即便词典里完全没有这个词,算法也能利用统计学规律和上下文的概率,"猜"出它可能是一个完整的词,并将其作为一个整体切分出来。

返回值:

  • 成功: cut 返回生成器(generator);lcut 返回由词汇字符串组成的列表(liststr)。

示例:

python 复制代码
import jieba

text = "清华大学的计算机科学与技术专业"
# 1.精确模式 (返回生成器,转化为list以便查看)
seg_gen = jieba.cut(text, cut_all=False)
print(list(seg_gen))
# ['清华大学', '的', '计算机科学', '与', '技术', '专业']

# 2.全模式 (直接返回列表)
seg_list = jieba.lcut(text, cut_all=True)
print(seg_list)
# ['清华', '清华大学', '华大', '大学', '的', '计算', '计算机', '计算机科学', '算机', '科学', '与', '技术', '专业']

作用:在精确模式的基础上,对发现的长词再次进行切分,提取出更细粒度的词汇,通常用于构建搜索引擎的倒排索引。cut_for_search 返回一个可迭代的生成器; lcut_for_search 则直接将全部分词结果转化为列表返回。

python 复制代码
jieba.cut_for_search(sentence, HMM=True)
jieba.lcut_for_search(sentence, HMM=True)

参数:

  • 文本 (sentence): 需要分词的目标字符串(str)。
  • 隐马尔可夫模型 (HMM): 是否开启 HMM 模型。默认为 True(bool)。

返回值:

  • 成功: cut_for_search 返回生成器(generator);lcut_for_search 返回列表(liststr)。

示例:

python 复制代码
import jieba

text = "清华大学的计算机科学与技术专业"
seg_search_gen = jieba.cut_for_search(text)
print("搜索引擎模式 (generator):", list(seg_search_gen))
# ['清华', '华大', '大学', '清华大学', '的', '计算', '算机', '科学', '计算机', '计算机科学', '与', '技术', '专业']
search_list = jieba.lcut_for_search(text)
print("搜索引擎模式 (list):", search_list)
# ['清华', '华大', '大学', '清华大学', '的', '计算', '算机', '科学', '计算机', '计算机科学', '与', '技术', '专业']

六.自定义词典与词频干预方法

在垂直领域(如医疗、法律、特定小说)中,系统自带的词典往往无法正确识别专业名词。此时需要介入干预 Jieba 的词库。

1. load_userdict - 加载外部自定义词典

作用:批量加载本地的自定义 TXT 词典文件,提升特定领域词汇的分词准确率。

python 复制代码
jieba.load_userdict(file_name)

参数:

  • 文件路径 (file_name): 包含自定义词汇的文本文件路径(str 或 pathlib.Path)。

词典文件格式要求: 一词占一行;每一行分为三部分:词语、词频(可省略)、词性(可省略),各部分之间用空格隔开。

示例:创新工场 3 n

返回值:

  • 无返回值,直接修改全局 jieba.dt 的内部状态。

2. add_word & del_word - 动态增删词汇

作用:在程序运行过程中,通过代码动态地向内存中的词典添加或移除特定词汇,而无需修改本地文件。

python 复制代码
jieba.add_word(word, freq=None, tag=None)
jieba.del_word(word)

参数:

  • 词汇 (word): 需要添加或删除的目标词语(str)。
  • 词频 (freq): 可选,指定该词的词频(int)。
  • 词性 (tag): 可选,指定该词的词性(str)。

示例:

python 复制代码
text = "我来到了庆国京都"
print(jieba.lcut(text)) # 可能被切成 ['我', '来到', '了', '庆国', '京都']

jieba.add_word("庆国京都")
print(jieba.lcut(text)) # 变为 ['我', '来到', '了', '庆国京都']

3. suggest_freq - 调节单个词语词频

作用:自动计算并调节单个词语的词频,强制让系统能够(或不能)将某几个字作为一个词分出来。这是解决"误拆分"或"误合并"的最直接方法。

python 复制代码
jieba.suggest_freq(segment, tune=True)

参数:

  • 词段 (segment): 需要干预的词汇。(str 或 tuple)。
    • 传入元组代表促使拆分,传入元组 (A, B) = "把 A 和 B 拆开,别连在一起!"
    • 传入单个字符串代表促使合并,传入字符串 "AB" = "把 A 和 B 锁死,这就是一个词!"
  • 调节启用 (tune): 是否立刻在内部词典中生效此词频调节。默认为 True(bool)。
    • tune=True:既计算,又执行(修改内存)。
    • tune=False:只计算,不执行(只看结果)。

返回值:

  • 成功: 返回调节后的该词语(或字组)的新词频(int)。

示例:

python 复制代码
import jieba

# 一.拆分场景:强制将一个词拆开 (tuple)
text = "如果放到post中将出错"
# --- 情况 A: tune=True (默认) ---
# 强制拆分,并立即在分词器中生效
jieba.suggest_freq(('中', '将'), tune=True)
print("tune=True 拆分结果:", jieba.lcut(text))
# 结果: ['如果', '放到', 'post', '中', '将', '出错']

# --- 情况 B: tune=False ---
# 此时调用 suggest_freq 仅仅是计算了词频,但没有强制覆盖内存中的模型,
# 因此分词效果可能不会发生改变,或者需要依赖后续更新
jieba.suggest_freq(('中', '将'), tune=False)
print("tune=False 拆分结果:", jieba.lcut(text))
# 结果: ['如果', '放到', 'post', '中将', '出错'] (效果未生效)

# 二.合并场景:强制将两个字锁死为一个词 (str)
text = "我爱北京天安门"
# 原始分词:系统可能会把 "天安门" 识别出来,但如果系统很笨,可能切成 ["天安", "门"]
print("原始分词:", jieba.lcut(text))
# --- 情况 A: tune=True ---
# 强制把 "天安门" 作为一个整体
jieba.suggest_freq("天安门", tune=True)
print("tune=True 合并结果:", jieba.lcut(text))
# 结果: ['我', '爱', '北京', '天安门']
# --- 情况 B: tune=False ---
# 调用了计算,但未应用到当前分词器的动态词典中
jieba.suggest_freq("天安门", tune=False)
print("tune=False 合并结果:", jieba.lcut(text))
# 结果: ['我', '爱', '北京', '天安', '门'] (可能保持原样,取决于模型初始状态)

七.关键词提取 - jieba.analyse子模块

需要额外 import jieba.analyse

1. extract_tags - 基于 TF-IDF 提取关键词

作用:基于统计学中的 TF-IDF 算法,从一段长文本中抽取出最具代表性的核心关键词。

python 复制代码
jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=())

参数:

  • 文本 (sentence): 待提取关键词的长文本字符串(str)。
  • 数量 (topK): 提取权重排名前 K 个的关键词。默认为 20(int)。
  • 权重返回 (withWeight): 是否一并返回每个关键词对应的 TF-IDF 权重值。默认为 False(bool)。
  • 词性过滤 (allowPOS): 仅包含指定词性的词(如仅提取名词和人名 ('n', 'nr'))。默认为空元组,即不筛选(tuple)。

返回值:

  • 成功: 默认返回关键词列表(liststr);如果 withWeight=True,则返回包含 (关键词, 权重) 的元组列表(listtuple)。

示例:

python 复制代码
import jieba.analyse

text = "人工智能与深度学习在自然语言处理领域取得了巨大的突破"
tags = jieba.analyse.extract_tags(text, topK=3, withWeight=True)
print(tags)
# [('自然语言处理', 1.12...), ('深度学习', 0.89...), ('人工智能', 0.75...)]

八.词性标注 - jieba.posseg 子模块

1. posseg.lcut - 词性标注

作用:在对文本进行分词的同时,标注出每一个词语的词性(如 n 名词、v 动词、a 形容词等)。

需要额外 import jieba.posseg as pseg

python 复制代码
pseg.cut(sentence)
pseg.lcut(sentence)

参数:

  • 文本 (sentence): 需要标注词性的目标字符串(str)。

返回值:

  • 成功: 返回包含 pair(word, flag) 对象的生成器或列表。可以通过 .word.flag 提取。

示例:

python 复制代码
import jieba.posseg as pseg

words = pseg.lcut("我爱北京天安门")
for w in words:
    print(f"{w.word}: {w.flag}")
# 我: r (代词)
# 爱: v (动词)
# 北京: ns (地名)
# 天安门: ns (地名)

九.位置获取方法

1. tokenize - 获取词语位置索引

作用:进行切分,并同时返回词语在原文中的起止位置(索引)。

python 复制代码
jieba.tokenize(unicode_sentence, mode="default")

参数:

  • 文本 (unicode_sentence): 需要处理的文本字符串。
  • 模式 (mode): 默认为 'default'(精确模式),可设为 'search'(搜索引擎模式)(str)。

返回值:

  • 成功: 返回一个生成器,每个元素是一个元组 (词语, 起始索引, 结束索引)(Generator)。

示例:

python 复制代码
result = jieba.tokenize("自然语言处理")
for tk in result:
    print(f"词汇: {tk[0]}, 起始: {tk[1]}, 结束: {tk[2]}")
# 词汇: 自然语言, 起始: 0, 结束: 4
# 词汇: 处理, 起始: 4, 结束: 6

十.结合Jieba的PyTorch Embedding示例

使用《庆余年》文本作为语料库,通过 jieba 构建真实的词汇表,并利用这些真实数据来演示 nn.Embedding 的查表过程。

python 复制代码
import torch
import torch.nn as nn
import jieba

# 1.准备原始文本
text1 = """朝堂是谁的朝堂,天下又是谁的天下------庆帝的文武百官绝不敢轻易忤逆或直谏,\
夹缝里喘息,贪污,结党,企图吞噬一点点这肮脏血腥的权力,却落得个死无葬身之地,\
他想起赖明成,陈萍萍,那些一个个被处以极刑的大臣,他与承乾自孩提时代就见识过权力只是一把刀子,\
一场流血,一个个微不足道的死亡,于是他们恐惧又愤恨,他们开始认为阴谋诡计是一种力量,\
非要一刀见血,才是一次胜利,嬴的人才配活着,与野兽何异?他们在跟谁争,\
只有庆帝一个人把握着权柄,把它高高挂起,高于良知,高于品德,高于世间万物,\
他们竟然在争抢这样一个丑陋又卑鄙,散发着腐烂恶臭的东西。"""

text2 = """人心?那我亏大了,自抬身价罢了。你觉得他们会感激我,那都是一时的,\
他们求的是自己想要的东西,我满足了一时片刻,他们就想要有别的东西了,\
这点儿不长久的人心算什么人心。你看那些古来文人大家,那庄墨韩,人人追捧,\
高高在上,不容亵渎,一字换一城也毫不夸张,是笔墨纸张值钱,还是他们的名气?\
就像是最近给你送礼攀关系那些人一样的,他们为的不是一时的东西,我也不能做那一\
时的玩意儿。"李承泽收回目光,转过身来,他握紧了自己的一双手,直视着谢必安冰冷\
宛如冬夜般的眼睛,"必安,你得明白,东宫太子有父母,有名头,有朝臣,陛下,\
拥有这个天下的人和财权,我只有我自己,但是我要让你知道的是,这些他们有的,\
不见得一直有,我依然有我自己。"""

# 2.使用 jieba 构建全局词汇表 (Vocabulary)
# 2.1对所有文本进行分词,获取所有的 token
all_words = jieba.lcut(text1 + " " + text2)
# 2.2去重并排序(排序非必须,只是为了结果稳定)
unique_words = sorted(list(set([w for w in all_words if w.strip()])))
# 2.3构建 word -> index 的映射字典
word2idx = {"<PAD>": 0}  # 郑重声明 0 号索引是 PAD
for i, word in enumerate(unique_words):
    word2idx[word] = i + 1

vocab_size = len(word2idx)
d_model = 16  # 为了方便展示,我们将向量维度设为16
pad_index = 0
print(f"总词汇量 (Vocab Size): {vocab_size}") # 总词汇量(Vocab Size): 204

# 3. 实例化 Embedding 层
embed_layer = nn.Embedding(num_embeddings=vocab_size, embedding_dim=d_model, padding_idx=pad_index)
# 观察底层核心矩阵 weight
print(embed_layer.weight.shape) # torch.Size([204, 16])(行数对应词汇表大小)
# 验证padding_idx:第0行被强行锁定为全0
print(embed_layer.weight[0]) # tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],       grad_fn=<SelectBackward0>)

# 4. 构造数据并映射为ID序列(支持动态Batch Size)
raw_texts = [text1, text2]  # 将待处理文本放入列表中
encoded_texts = [] # 映射后ID序列列表
max_len = 0 # 最大句子长度,确定时间步长度用
# 4.1将文本分词、映射为ID序列,并动态记录最大长度
for text in raw_texts:
    # 转换为ID序列(过滤掉不在词典中的词,增加鲁棒性)
    encoded_sequence = [word2idx[word] for word in jieba.lcut(text) if word in word2idx]
    encoded_texts.append(encoded_sequence)
    # 确认并更新 max_len
    if len(encoded_sequence) > max_len:
        max_len = len(encoded_sequence)

# 4.2依据max_len对所有序列进行Padding填充
padded_sequences = []
for seq in encoded_texts:
    # 用PAD(0)补齐至最大长度
    padded_seq = seq + [pad_index] * (max_len - len(seq))
    padded_sequences.append(padded_seq)

# 4.3 组装成Batch输入张量(必须是torch.long类型)
input_indices = torch.tensor(padded_sequences, dtype=torch.long)

# 输入张量(Batch, Seq_Len)映射关系
print(input_indices)# tensor([[119, 116, ...(省略中间) , 0,   0],[ 42, 203,  ...(省略中间) , 156, 4]])
print(input_indices.shape) # torch.Size([2, 202])

# 5. 前向传播:执行查表操作
embedded_features = embed_layer(input_indices)
# Embedding处理后的输出
print(embedded_features.shape) # torch.Size([2, 202, 16])

需要注意的是,在实际构建Encoder时,Embedding 层既可以写在 Encoder 的结构中,也可以单独写。如果将Embedding层写在Encoder结构中,代码会整洁,所有与模型相关的部分都在同一个类中,便于维护,但这个操作相当于将整个Transformer结构的输入数据由(batch_size, seq_len, input_dimensions)结构修改为了(batch_size, seq_len)结构、从而会影响掩码的结构、影响掩码函数、这个Embedding层无法与Decoder或其他算法共用、甚至可能导致你的Encoder结构无法用于时间序列任务;如果我们将Embedding层写在Encoder结构之外,灵活性更高,可以在多个模型中共享同一个 Embedding 层,也可以对Embedding后的结果进行更加顺畅的独立处理,但缺点是就需要额外的代码来管理Embedding层的初始化和传递。在实际进行编程时,Embedding结构究竟写在Encoder内还是外,与你的数据和实现的架构有很大的关系,你需要根据具体情况进行具体的分析。