spaCy中文分句模型微调秘籍,从数据准备到模型评测,一学就会
🎯 文章目标
- 认识 spaCy 中文分句的三大方案(Sentencizer、senter、DependencyParser)及其适用场景
- 理解分句背后的原理、工程实现与模型结构(CNN/Transformer)
- 学会从数据准备、格式转换、模型训练到推理与评估的完整流程
- 掌握如何根据实际需求选择合适的 pipeline 组件和底层结构
- 拓展分句思想到 RAG 等复杂场景,实现高质量文本分块与智能预处理
本次文章配套代码地址:https://github.com/li-xiu-qi/XiaokeAILabs/tree/main/datas/spacy_finetune
后续可能会微调一个专门用来分块的模型,感兴趣的同学欢迎联系我或者给项目一个star哦,仓库地址:https://github.com/li-xiu-qi/spacy_chuking
📋 目录
- 1. 句子边界检测(SBD)在NLP中的基础性作用
- 🖨️ 读取 .spacy 文件并打印每个 doc 的句子分割结果
🚁 前言
在实际 NLP 项目中,中文分句往往是文本处理的第一步,但 spaCy 默认的分句效果在面对特殊标点、缩写、省略号、对话等复杂情况时,常常难以满足高精度需求。很多同学会问:spaCy 到底有哪些分句方案?如何选择最适合自己的分句方式?能不能既高效又智能地实现分句,甚至扩展到更复杂的文本分块?
本教程将系统梳理 spaCy 的分句原理与工程实现,带你从最基础的规则分句(Sentencizer)、到可训练的 senter 组件、再到依存句法驱动的 DependencyParser,逐步深入每种方案的优缺点和适用场景。你不仅能学会如何微调 spaCy 分句模型,还能理解不同模型结构(CNN/Transformer)的本质区别,掌握如何根据实际需求灵活选择 pipeline 组件。更重要的是,本文还将带你思考如何将分句思想扩展到 RAG 等智能分块场景,助力你的文本处理系统更上一层楼。
🧠 spaCy句子分割理论与工程基础补充
1. 句子边界检测(SBD)在NLP中的基础性作用
句子边界检测(Sentence Boundary Detection, SBD)是自然语言处理(NLP)中的基础模块,其准确性直接影响分词、词性标注、依存句法、命名实体识别等下游任务。SBD的难点在于标点符号的歧义性(如缩写、小数点、省略号等),这使得“遇到句号就分割”远远不够,必须结合上下文、词性、大小写等多维特征。现代NLP通常将SBD视为一个分类问题,针对每个潜在边界点,利用统计学习或神经网络模型进行判别。
SBD主流方法简述
- spaCy的senter和parser组件正是吸收了这些主流思想的产物
多语言与领域适应性
不同语言和专业领域(如医学、法律、金融)对SBD有不同挑战。spaCy支持多语言和可定制微调,能适应多样化场景。
2. spaCy三大句子分割策略对比
- DependencyParser:基于依存句法,最强大但最慢,适合需要句法树的场景。
- senter:专用分句器,速度快、精度高,适合只需分句的任务。
- Sentencizer:基于规则,极快但不智能,适合格式规范文本。
在实际应用中,Sentencizer 是 spaCy 提供的基于规则的分句器,它无需任何训练和标注数据,只依赖简单的标点规则(如句号、问号、感叹号等)进行分句,非常适合规则明确、对分句精度要求不高的场景。你只需在 pipeline 里加上sentencizer组件即可直接使用。例如:
importspacy
nlp = spacy.blank("zh")
nlp.add_pipe("sentencizer")
doc = nlp("今天天气很好。我想去公园。你呢?")
forsentindoc.sents:
print(sent.text)
Sentencizer 开箱即用,无需训练,适合快速分句需求。
选择建议:
- 需要依存句法分析时用DependencyParser
- 追求极致速度且文本结构简单时用Sentencizer
- 本文教程聚焦于senter分句器的训练与微调,未涉及DependencyParser的训练。原因在于:
- senter组件专为句子边界检测设计,训练和部署都更为高效,适合大多数只需分句的NLP场景。
- DependencyParser不仅进行分句,还会进行完整的句法依存分析,适用于需要深入理解句法结构的复杂任务(如关系抽取、深层语义分析等),其训练和推理资源消耗更大。
- 对于一般的文本分句需求,senter已能满足高精度、快速分割的要求,无需引入更复杂的依存分析。
在 spaCy 的 senter 组件中,默认采用的是 CNN(如 HashEmbedCNN)结构实现。你也可以通过修改 config 文件,切换为 transformer 或其他结构,但默认就是 CNN。CNN 结构需配置 window_size,决定每个 token 能看到的上下文范围;而 transformer 结构无需配置窗口,能自动建模全局上下文(只受最大 token 长度限制)。如果你的分句任务主要依赖于局部标点和短距离上下文,CNN结构即可满足需求且效率高;如果文本结构复杂、长距离依赖明显,或希望获得更强的泛化能力,建议优先尝试Transformer结构。
此外,spaCy 的 senter 或 transformer+senter 组件在推理时,是将整段文本(如一整个句子或段落)分词后,作为一个整体输入模型,而不是每次只输入几个单词。spaCy 会自动处理超长文本的切分,最大 token 数由 transformer 模型决定(如 512)。这意味着 spaCy senter/transformer 是“整段文本分词后一起输入模型”,不是每次只输入3或5个词(窗口大小)。
parser 组件的分句原理
当 pipeline 里有 parser(依存句法分析器)时,spaCy 会先预测每个 token 的依存关系,构建依存树,再结合规则自动判断句子边界(如遇到 ROOT 节点、特定标点等)。这种方式能处理复杂句法结构,比单纯靠标点分句更智能。例如:“他说:你去吧,然后回来。”,parser 能识别“你去吧”是独立分句,即使没有明显句号。
spaCy 默认的句法依存分析器(parser)最早是基于 CNN+特征嵌入(HashEmbedCNN)和动态过渡系统(transition-based parser)实现的。但在新版 spaCy(v3 及以后),parser 也可以用 transformer 作为特征提取器(tok2vec),即“transformer+transition-based parser”结构。你可以在 config 文件里选择 tok2vec 用 CNN 还是 transformer。
总结来说,spaCy 的依存分析器既可以用 CNN,也可以用 transformer,取决于你的配置。
如果你用 transformer+senter 进行句子分割,通常不需要再依赖句法依存分析(parser)来分句。transformer+senter 已经能利用全局上下文和强大的特征表达能力,通常分句效果比传统 CNN 更好,适合复杂文本。senter 组件专门为句子边界识别而设计,训练目标就是分句,不依赖依存结构。只有在你还需要做依存句法分析(比如后续要用依存树做信息抽取、关系抽取等任务)时,才需要加 parser 组件。
🗺️ 整体流程概览
- 🏋️♂️ 模型训练(标准/Transformer)
🪄 微调难度说明
微调 spaCy 的句子分割器相对简单,主要流程包括数据准备、配置文件初始化、模型训练和评估。spaCy 已经为我们封装了大部分底层实现细节。我们只需准备好特定格式的数据(如 jsonl 或 spacy 格式),并通过简单的配置文件指定训练参数,无需手动编写复杂的深度学习训练代码。整个微调流程高度自动化,极大降低了上手门槛,下面从环境配置开始,注意不要使用过高的python版本,否则会导致一些库的兼容问题。
🛠️ 环境准备
建议使用 Python 3.10 及以下版本。推荐新建虚拟环境,并安装 spaCy 及相关依赖:
python -m venv .venv
source.venv/bin/activate
pip install -r tests/requirements.txt
📑 数据准备与格式说明
训练数据需为 spaCy 支持的 jsonl 或 spacy 格式。每条数据应包含文本及句子边界标注。
具体可以参考下面的示例:
{"text":"今天天气很好。我想去公园。","tokens": ["今天天气","很","好","。","我","想","去","公园","。"],"sent_starts": [true,false,false,false,true,false,false,false,false]}
{"text":"小明喜欢看书,也喜欢运动。你呢?","tokens": ["小明","喜欢","看书",",","也","喜欢","运动","。","你","呢","?"],"sent_starts": [true,false,false,false,false,false,false,false,true,false,false]}
这里的数据格式是 spaCy 句子分割器(senter)训练所需的 jsonl 格式,每一行为一个 JSON 对象,包含两个字段:
- sent_starts:一个与文本中每个 token 对应的布尔值列表,标记每个 token 是否为句子的起始(true 表示该 token 是一个新句子的开头,false 表示不是)。
举例说明:
{"text": "今天天气很好。我想去公园。", "tokens": ["今天天气", "很", "好", "。", "我", "想", "去", "公园", "。"], "sent_starts": [true, false, false, false, true, false, false, false, false]}
字段解释:
tokens:对原始文本进行分词后的结果,是一个字符串列表,每个元素为一个 token(词或符号)。sent_starts:与tokens等长的布尔值列表,标记每个 token 是否为句子的起始。true表示该 token 是新句子的开头,false表示不是。
例如,上面例子中:
"今天天气"对应true,表示是第一个句子的开头;- 其余 token 对应
false,表示不是句子起始。
这样 spaCy 就能根据sent_starts信息,学习如何在中文文本中正确分句。
🔄 jsonl 转 spaCy 格式脚本
如果你需要将上述 jsonl 格式的数据转换为 spaCy 所需的 .spacy 格式,可以参考如下脚本:
importspacy
fromspacy.tokensimportDoc, DocBin
importjson
nlp = spacy.blank("zh")
doc_bin = DocBin()
withopen("senter_train.jsonl","r", encoding="utf-8")asf:
forlineinf:
example = json.loads(line)
tokens = example["tokens"]
sent_starts = example["sent_starts"]
iflen(tokens) != len(sent_starts):
raiseValueError(f"tokens 数与 sent_starts 不一致:{tokens}\nsent_starts:{sent_starts}")
doc = Doc(nlp.vocab, words=tokens)
fori, tokeninenumerate(doc):
token.is_sent_start = sent_starts[i]
doc_bin.add(doc)
doc_bin.to_disk("senter_train.spacy")
print("已生成 senter_train.spacy ")
脚本会读取 jsonl 文件,生成 spaCy 训练所需的二进制数据文件(.spacy)。其作用是:将标注了句子起始信息的 jsonl 格式数据,转换为 spaCy 训练所需的二进制格式(.spacy文件),用于训练 spaCy 的句子分割器(senter)。
Doc是 spaCy 的核心数据结构之一,表示经过分词和注解的一段文本。本质上是一个“文档对象”,包含分词后的 token 列表及各种语言学特征(如词性、句法、实体等)。
在本脚本中,Doc对象的创建方式如下:
doc = Doc(nlp.vocab, words=tokens)
nlp.vocab:当前语言环境的词汇表(vocab),支持 spaCy 的各种词汇特性。words=tokens:分词后的 token 列表(字符串列表),每个元素对应文本中的一个 token。
这样创建的Doc对象包含了自定义的分词结果,后续可为每个 token 设置句子起始标记(is_sent_start),用于训练 spaCy 的句子分割模型。
DocBin是 spaCy 提供的高效二进制序列化工具,用于批量存储和管理多个Doc对象。它可将大量Doc文档对象打包成一个二进制文件(如.spacy),便于模型训练和分发。
在本脚本中,DocBin对象的创建方式如下:
doc_bin = DocBin()
每处理好一个Doc,通过doc_bin.add(doc)加入容器。最后用doc_bin.to_disk("senter_train.spacy")一次性保存为 spaCy 训练所需的二进制格式文件。
🖨️ 读取 .spacy 文件并打印每个 doc 的句子分割结果
你可以用下面的代码快速验证 .spacy 文件中的分句效果:
importspacy
fromspacy.tokensimportDocBin
# 读取 .spacy 文件并打印每个 doc 的句子分割结果
nlp = spacy.blank("zh")
doc_bin = DocBin().from_disk("senter_train.spacy")
docs = list(doc_bin.get_docs(nlp.vocab))
fori, docinenumerate(docs):
print(f"Doc{i+1}:")
forsentindoc.sents:
print(f" 句子:{sent.text}")
print("-"*20)
输出:
Doc 1:
句子: 今天天气 很 好 。
句子: 我 想 去 公园 。
--------------------
Doc 2:
句子: 小明 喜欢 看书 , 也 喜欢 运动 。
句子: 你 呢 ?
--------------------
这样可以直接看到每个 doc 的分句效果,方便检查数据标注和格式是否正确。
在上面代码中,get_docs(nlp.vocab)的作用是:
doc_bin.get_docs(nlp.vocab)会根据你传入的nlp.vocab(即 spaCy 的词汇表对象),把二进制的.spacy文件内容还原为一个个Doc文档对象。- 由于 spaCy 的
DocBin序列化时只保存了 token 信息等内容,反序列化时需要传入当前的Vocab(词汇表)来正确构建每个Doc。 - 这样你就可以像平时一样遍历、分析这些
Doc,比如用doc.sents获取分句结果。
简而言之,get_docs(nlp.vocab)就是“用当前的词汇表,把二进制数据还原成 spaCy 的文档对象列表”,是 spaCy 官方推荐的标准用法。
⚙️ 配置文件初始化
spaCy 训练依赖 config 文件。推荐用命令自动生成:
python -m spacy init config config.cfg --pipeline senter --lang zh
python -m spacy init config:初始化 spaCy 配置文件。--pipeline senter:只训练句子分割(senter)组件。
📝 config 文件定义了训练流程、模型结构、超参数,更多可选的参数请看官方文档等。
运行后会输出一个内容大概是这样的config.cfg文件:
[paths]
train = null
dev = null
vectors = null
init_tok2vec = null
[system]
gpu_allocator = null
seed = 0
[nlp]
lang ="zh"
pipeline = ["senter"]
batch_size = 1000
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
vectors = {"@vectors":"spacy.Vectors.v1"}
[nlp.tokenizer]
@tokenizers ="spacy.zh.ChineseTokenizer"
segmenter ="jieba"
[components]
[components.senter]
factory ="senter"
overwrite =false
scorer = {"@scorers":"spacy.senter_scorer.v1"}
[components.senter.model]
@architectures ="spacy.Tagger.v2"
nO = null
normalize =false
[components.senter.model.tok2vec]
@architectures ="spacy.HashEmbedCNN.v2"
pretrained_vectors = null
width = 12
depth = 1
embed_size = 2000
window_size = 1
maxout_pieces = 2
subword_features =true
[corpora]
[corpora.dev]
@readers ="spacy.Corpus.v1"
path =${paths.dev}
max_length = 0
gold_preproc =false
limit= 0
augmenter = null
[corpora.train]
@readers ="spacy.Corpus.v1"
path =${paths.train}
max_length = 0
gold_preproc =false
limit= 0
augmenter = null
[training]
dev_corpus ="corpora.dev"
train_corpus ="corpora.train"
seed =${system.seed}
gpu_allocator =${system.gpu_allocator}
dropout = 0.1
accumulate_gradient = 1
patience = 1600
max_epochs = 0
max_steps = 20000
eval_frequency = 200
frozen_components = []
annotating_components = []
before_to_disk = null
before_update = null
[training.batcher]
@batchers ="spacy.batch_by_words.v1"
discard_oversize =false
tolerance = 0.2
get_length = null
[training.batcher.size]
@schedules ="compounding.v1"
start = 100
stop = 1000
compound = 1.001
t = 0.0
[training.logger]
@loggers ="spacy.ConsoleLogger.v1"
progress_bar =false
[training.optimizer]
@optimizers ="Adam.v1"
beta1 = 0.9
beta2 = 0.999
L2_is_weight_decay =true
L2 = 0.01
grad_clip = 1.0
use_averages =false
eps = 0.00000001
learn_rate = 0.001
[training.score_weights]
sents_f = 1.0
sents_p = 0.0
sents_r = 0.0
[pretraining]
[initialize]
vectors =${paths.vectors}
init_tok2vec =${paths.init_tok2vec}
vocab_data = null
lookups = null
before_init = null
after_init = null
[initialize.components]
[initialize.tokenizer]
pkuseg_model = null
pkuseg_user_dict ="default"
🏋️♂️ 训练模型
基于默认的模型进行训练
训练集和验证集都用同一份数据,因为我们这里是示例:
python -m spacy train config.cfg \
--paths.train data/senter_train.spacy \
--paths.dev data/senter_train.spacy \
--output output_model
python -m spacy train config.cfg:用 config 文件启动训练。
训练过程会输出日志,最终模型在output_model/model-best。
🤖 使用 Transformer 训练
如果你希望用 transformer 结构进行句子分割微调,可以使用如下命令(需准备好 transformer 配置文件 config_trf.cfg)。
使用 GPU 训练:
python -m spacy train config_trf.cfg \
--gpu-id 0 \
--paths.train data/senter_train.spacy \
--paths.dev data/senter_train.spacy \
--output output_model_trf
使用 CPU 训练:
python -m spacy train config_trf.cfg \
--paths.train data/senter_train.spacy \
--paths.dev data/senter_train.spacy \
--output output_model_trf
config_trf.cfg:基于 transformer 的 spaCy 配置文件。--gpu-id 0:指定使用第 0 块 GPU 训练(如无 GPU 可省略)。
📝 Transformer 配置文件示例
下面是一个 spaCy Transformer 配置文件(config_trf.cfg)示例,适用于中文句子分割任务,并附有详细注释:
# [paths] 部分用于定义关键文件的路径,在训练时通过命令行传入。
[paths]
train = null
dev = null
vectors = null
init_tok2vec = null
# [system] 部分用于配置训练的硬件和可复现性。
[system]
# gpu_allocator: 设置使用的GPU内存分配器。
# "pytorch" 是推荐值。设为 null 或 none 则使用CPU。
gpu_allocator = pytorch
# seed: 随机种子,用于确保每次训练结果的可复现性。
seed = 0
# [nlp] 部分是流水线(pipeline)的核心配置。
[nlp]
lang = "zh"
# pipeline: 定义组件的处理顺序。这里,文本会先经过 transformer 组件,
# 然后其输出会作为 senter 组件的输入。顺序至关重要。
pipeline = ["transformer", "senter"]
# batch_size: 一次处理的文本数量。可根据内存大小调整。
batch_size = 1000
disabled = []
before_creation = null
after_creation = null
after_pipeline_creation = null
[nlp.tokenizer]
# @tokenizers: 指定中文分词器。
@tokenizers = "spacy.zh.ChineseTokenizer"
segmenter = "jieba"
# ===================================================================
# 组件 (Components) 配置
# ===================================================================
[components]
# [components.transformer] 定义了 Transformer 组件本身。
[components.transformer]
factory = "transformer"
[components.transformer.model]
@architectures = "spacy-transformers.TransformerModel.v3"
# name: 指定要使用的 Hugging Face Hub 上的预训练模型。
# 例如 "bert-base-chinese" 或 "hfl/chinese-roberta-wwm-ext"。
# spaCy 会在首次运行时自动下载。
name = "bert-base-chinese"
tokenizer_config = {"use_fast": true}
# [components.senter] 定义了句子切分器 (Sentence Recognizer)。
[components.senter]
factory = "senter"
overwrite = false
scorer = {"@scorers":"spacy.senter_scorer.v1"}
[components.senter.model]
@architectures = "spacy.Tagger.v2"
nO = null
normalize = false
[components.senter.model.tok2vec]
# @architectures: 指定 tok2vec (token-to-vector) 的架构。
# "TransformerListener" 是一个特殊组件,它本身不产生向量,
# 而是“监听”并直接使用上游 "transformer" 组件生成的、带有上下文信息的向量。
@architectures = "spacy-transformers.TransformerListener.v1"
grad_factor = 1.0
# pooling: 定义如何将一个词的多个子词(word-piece)的向量聚合成一个单一向量。
# "reduce_mean.v1" 表示使用平均值策略。
pooling = {"@layers":"reduce_mean.v1"}
# ===================================================================
# 语料库 (Corpora) 配置
# ===================================================================
[corpora]
[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0
gold_preproc = false
limit = 0
augmenter = null
[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0
gold_preproc = false
limit = 0
augmenter = null
# ===================================================================
# 训练 (Training) 配置
# ===================================================================
[training]
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"
seed = ${system.seed}
gpu_allocator = ${system.gpu_allocator}
# dropout: 在训练期间随机丢弃一部分神经元,是防止过拟合的常用技巧。
dropout = 0.1
# accumulate_gradient: 梯度累积。如果显存不足,可设为 >1 的整数(如2, 4)。
# 这会用更多训练时间来换取更低的显存消耗。
accumulate_gradient = 1
# patience: 早停机制。如果在连续 1600 步内模型性能都没有提升,训练将自动停止。
patience = 1600
max_epochs = 0
# max_steps: 最大训练步数。若设为 0,则由 max_epochs 控制训练长度。
max_steps = 20000
# eval_frequency: 每训练多少步,就在开发集上评估一次性能。
eval_frequency = 200
frozen_components = []
annotating_components = []
before_to_disk = null
before_update = null
[training.batcher]
# @batchers: 定义如何将数据打包成批次 (batch)。
# "spacy.batch_by_words.v1" 会尽量让每个批次包含差不多数量的词。
@batchers = "spacy.batch_by_words.v1"
discard_oversize = false
tolerance = 0.2
get_length = null
[training.batcher.size]
# @schedules: 定义批次大小的变化策略。
# "compounding.v1" 表示批次大小从 start 值平滑增长到 stop 值。
@schedules = "compounding.v1"
start = 100
stop = 1000
compound = 1.001
t = 0.0
[training.logger]
@loggers = "spacy.ConsoleLogger.v1"
progress_bar = true
[training.optimizer]
@optimizers = "Adam.v1"
beta1 = 0.9
beta2 = 0.999
L2_is_weight_decay = true
L2 = 0.01
grad_clip = 1.0
use_averages = false
eps = 0.00000001
# learn_rate: 学习率。对于 Transformer 微调,一个较小的值(如 2e-5)是很好的起点。
learn_rate = 2e-5
[training.score_weights]
# sents_f: 评估时使用的主要分数,这里是句子切分的 F1-score。
sents_f = 1.0
sents_p = 0.0
sents_r = 0.0
[pretraining]
# [initialize] 部分用于在训练开始前初始化组件。
[initialize]
vectors = null
init_tok2vec = null
vocab_data = null
lookups = null
before_init = null
after_init = null
[initialize.components]
如需自定义 transformer 预训练模型、分词方式,可在此基础上修改。
🤔 Transformer 预训练模型选择建议
bert-base-chinese是通用的中文 Transformer 预训练模型,适合大多数场景。hfl/chinese-roberta-wwm-ext是哈工大开源的中文 RoBERTa-wwm 扩展版,在许多中文任务上表现优于 BERT,推荐在资源允许时优先尝试。- 其他可选模型如
hfl/chinese-macbert-base、uer/chinese-roberta-base等,也可根据实际需求和硬件资源选择。 - 注意:更大的模型(如 RoBERTa-wwm-ext)通常带来更好的效果,但显存和训练时间消耗也更高。
🛠️ 关键超参数调优指南
learn_rate(学习率):控制模型参数更新的步幅。Transformer 微调推荐较小的学习率(如 2e-5 ~ 5e-5),过大易导致训练不稳定,过小则收敛慢。dropout(随机丢弃率):防止过拟合,常用值为 0.1~0.3。增大 dropout 可提升泛化能力,但过大可能影响模型表达能力。patience(早停容忍步数):连续多少步验证集无提升则提前停止训练。patience 设大可获得更充分训练,设小则可加快实验迭代。- 其他如
batch_size、max_steps也可根据显存和数据量适当调整。
合理选择模型和调优超参数,有助于在硬件资源允许的前提下获得最佳分句效果。
⚡ 使用 transformer 结构通常能获得更好的效果,但对显存和硬件要求更高。 根据我的本地环境测试,GPU为A6000,代码如果使用cpu训练cnn版本的模型,大概1分钟内可以完成训练,而训练transformer版本的模型需要使用GPU训练大概几分钟,如果是CPU可能需要十几分钟。不过本次文章的超参数是我乱设置的,实际训练的时候建议调整一下。
🧪 模型推理与效果展示
训练完成后,可以直接加载模型进行分句推理。下面分别演示如何加载标准模型和 transformer 结构训练的模型,并输出分句结果:
importspacy
# 加载标准模型
test_text ="今天天气很好。我想去公园。"
nlp = spacy.load("output_model/model-best")
doc = nlp(test_text) # 直接用pipeline处理原始文本
print("标准模型分句结果(spaCy自带分词+完整pipeline):")
print("
ipeline 组件:", nlp.pipe_names)
fortokenindoc:
print(token.text, token.is_sent_start)
print("\n分句结果:")
forsentindoc.sents:
print(sent.text)
# 加载 transformer 结构训练的模型
nlp_trf = spacy.load("output_model_trf/model-best")
doc_trf = nlp_trf(test_text) # 直接用pipeline处理原始文本
print("\nTransformer 模型分句结果(spaCy自带分词+完整pipeline):")
print("
ipeline 组件:", nlp_trf.pipe_names)
fortokenindoc_trf:
print(token.text, token.is_sent_start)
print("\n分句结果:")
forsentindoc_trf.sents:
print(sent.text)
输出:
标准模型分句结果(spaCy自带分词+完整pipeline):
Pipeline 组件: ['senter']
今天天气 True
很 False
好 False
。 False
我 True
想 False
去 False
公园 False
。 False
分句结果:
今天天气很好。
我想去公园。
Transformer 模型分句结果(spaCy自带分词+完整pipeline):
Pipeline 组件: ['transformer','senter']
今天天气 True
很 False
好 False
。 False
我 True
想 False
去 False
公园 False
。 False
分句结果:
今天天气很好。
我想去公园。
上述代码会分别输出标准模型和 transformer 微调模型的分句效果,便于直观对比模型分割效果。
📝 代码说明
nlp对象:spaCy 的语言处理管道对象,包含分词、句子分割等组件。通过spacy.load()加载训练好的模型后,nlp就可以直接对原始文本进行处理。doc对象:spaCy 的 Doc 类型,是对文本的结构化表示,包含分词、句子、标注等信息。doc可以像列表一样遍历每个 token,也可以通过doc.sents获取分句结果。pipe_names:nlp.pipe_names属性返回当前 pipeline 中所有组件的名称列表,比如['transformer', 'senter'],便于了解模型结构。token对象:doc中的每个元素都是一个 token(词或符号),可以通过token.text获取文本,通过token.is_sent_start判断该 token 是否为句子起始。sents:doc.sents是 spaCy Doc 对象的一个生成器,表示分句后的句子序列。遍历doc.sents可以依次获得每个句子的文本(sent.text),用于查看模型的分句效果。
下面通过一个实际代码示例,帮助理解 nlp、doc、pipe_names、token、sents 的用法:
importspacy
# 加载模型
nlp = spacy.load("output_model/model-best")
# nlp 对象:可以直接处理原始文本
text ="小明喜欢看书,也喜欢运动。你呢?"
doc = nlp(text)
# pipe_names:查看 pipeline 组件
print("
ipeline 组件:", nlp.pipe_names) # 例如 ['transformer', 'senter']
# doc 对象:结构化文本,支持遍历 token
print("所有 token:")
fortokenindoc:
print(f"{token.text}\t是否句子起始:{token.is_sent_start}")
# token 对象:每个 token 的属性
first_token = doc[0]
print(f"第一个 token:{first_token.text}, 是否句子起始:{first_token.is_sent_start}")
# sents:遍历分句结果
print("\n分句结果:")
forsentindoc.sents:
print(sent.text)
输出如下:
Pipeline 组件: ['senter']
所有 token:
小明 是否句子起始: True
喜欢 是否句子起始: False
看书 是否句子起始: False
, 是否句子起始: False
也 是否句子起始: False
喜欢 是否句子起始: False
运动 是否句子起始: False
。 是否句子起始: False
你 是否句子起始: True
呢 是否句子起始: False
? 是否句子起始: False
第一个 token: 小明, 是否句子起始: True
分句结果:
小明喜欢看书,也喜欢运动。
你呢?
如果换成transformer模型,那么输出结果会变成这样:
Pipeline 组件: ['transformer','senter']
所有 token:
小明 是否句子起始: True
喜欢 是否句子起始: False
看书 是否句子起始: False
, 是否句子起始: False
也 是否句子起始: False
喜欢 是否句子起始: False
运动 是否句子起始: False
。 是否句子起始: False
你 是否句子起始: True
呢 是否句子起始: False
? 是否句子起始: False
第一个 token: 小明, 是否句子起始: True
分句结果:
小明喜欢看书,也喜欢运动。
你呢?
通过这个例子可以直观理解 spaCy 推理流程中各对象的作用和输出。
📈 微调效果与评估
微调后,模型能更准确地识别句子边界,减少误分割和漏分割。例如,能更好地处理缩写、省略号、特殊标点、特殊表达等情况。实际效果可通过在自有数据集上的评估指标(如准确率、召回率、F1 分数等)体现。
如果你需要对分句模型进行评测,推荐的做法如下:
- 评测集应与训练集分开,包含真实场景下的文本及人工标注的句子边界。
- 将模型输出的分句结果与人工标注的标准答案进行对比。
分句常用评估指标:
公式如下:
其中:
你可能会觉得这些评估指标的名字(如“精确率”“召回率”)有点“怪”,因为分句任务和传统的分类、检索任务不太一样。其实这里的“精确率”“召回率”是借用信息检索领域的通用定义:
虽然分句不是传统意义上的“检索”或“分类”,但本质上也是“判断每个 token 是否为句子开头”的二分类问题,所以这些指标依然适用。spaCy 评测时也是用这些通用指标来衡量分句效果。
- 精确率(Precision):模型预测为“句子起始点”的位置中,有多少是真正的句子起始。
- 召回率(Recall):所有真实的句子起始点中,有多少被模型找出来了。
- F1 分数:综合衡量模型“找得准”和“找得全”的能力。
- 精确率(Precision):被模型判为句子边界的结果中,实际为边界的比例。
- 召回率(Recall):所有真实句子边界中,被模型正确识别的比例。
- F1 分数(F1 Score):精确率和召回率的调和平均,更全面反映模型性能。
- 准确率(Accuracy):整体预测正确的比例(可选,分句任务更常用 F1/精确率/召回率)。
评测代码示例(spaCy Scorer 版):
你可以直接用 spaCy 自带的Scorer工具对训练数据进行分句评测,代码如下:
importspacy
fromspacy.tokensimportDocBin
fromspacy.scorerimportScorer
fromspacy.trainingimportExample
# 加载训练好的 spaCy 分句模型
nlp = spacy.load("output_model/model-best")
# 加载标注好的分句数据(.spacy 格式)
doc_bin = DocBin().from_disk("data/senter_train.spacy")
docs = list(doc_bin.get_docs(nlp.vocab))
scorer = Scorer() # 初始化评测器
examples = [] # 存放 Example 对象
fordocindocs:
pred_doc = nlp(doc.text) # 用模型对原文重新分句
example = Example(pred_doc, doc) # 构造评测用的 Example 对象
examples.append(example)
# 评测所有 Example,返回各项指标
results = scorer.score(examples)
print("分句评测结果:")
print(results)
输出示例:
分句评测结果:
{'token_acc': None,'token_p': None,'token_r': None,'token_f': None,'sents_p': 1.0,'sents_r': 1.0,'sents_f': 1.0, ...}
结果解读:
需要注意的是,spaCy Scorer 并不会输出“准确率(accuracy)”这一指标。这是因为分句任务中,大多数 token 都不是句子起始点(即负例远多于正例),如果用准确率,模型即使全部预测为“不是句子起始”,准确率也会很高,但模型其实没有学到分句能力。因此,准确率不能真实反映分句模型的实际效果。spaCy 的 Scorer 工具更关注查准率(精确率)、查全率(召回率)和 F1 分数,这些指标能更科学地衡量分句模型的实际能力。
sents_p、sents_r、sents_f分别表示分句的精确率、召回率和 F1 分数,1.0 表示分句完全正确。- 其它如
token_acc、ents_f等为 None,说明本次只评测了分句,不涉及分词、实体等任务。 - 只要
sents_f、sents_p、sents_r越接近 1,说明分句模型效果越好。
📏 本文以流程演示为主,实际评测可直接真实的评测数据进行。
🧩 RAG 分块扩展思考:从分句到智能分块
在 RAG(Retrieval-Augmented Generation)系统中,文本分块(chunking)是比分句更关键的预处理环节。高质量的分块能显著提升检索与生成效果。我们能否将 spaCy 的分句能力扩展为“智能分块”呢?以下是我们的思考与实践建议:
分块的核心思想
- spaCy 的分句本质是判断某个符号(如标点)是否适合作为分句点。
- RAG 分块可以直接扩展为“判断某个符号/位置是否适合作为分块点”,本质上是同一类序列决策问题,只是目标不同。
- 可以训练模型直接预测哪些位置适合分块,而不是先分句再合并。
- 这样可以灵活适配各种场景,并结合上下文、结构等多种信号。
实现路径
- 收集与目标 RAG 场景相关的文档,人工标注最佳分块点。 -以 spaCy 分句类似的方式,为每个潜在分块点(如标点、换行等)打上“是否为分块点”的标签。
- 原文:
机器学习是人工智能的一个分支。它致力于研究计算机如何模拟或实现人类的学习行为。近年来,深度学习作为其一个重要领域,取得了突破性进展,尤其是在计算机视觉和自然语言处理方面。这得益于算力的提升和海量数据的出现。 - 分块点标注:
[True, False, True, False, False, ...] # 仅示意,True 表示当前位置可分块
实际 RAG 应用中,分块点的判断不应仅依赖标点或简单规则。比如:
- 角色对话:同一角色连续发言,即使有多个句号,也不应分块,避免切断语义完整的表达。
- 诗歌/歌词/古文:分块点应结合结构、韵律或语义,而非单纯标点。
- 技术文档/代码:分块点可结合标题、缩进、格式等多种信号。
因此,分块点的判断策略应根据具体场景灵活设计,既可用规则,也可用模型学习,甚至两者结合。
这种方法本质上是将 spaCy 分句思想扩展到更高层次的“分块点判断”,能为 RAG 应用带来更智能、更贴合业务需求的分块能力。但是模型的输入序列长度也是有限的,似乎还是需要进行预先分块,而不是无论多长都能直接输入模型进行处理,这个问题我觉得值得我们进一步去思考如何处理。
📚 不同模型结构的上下文感知能力说明
- 需要配置窗口大小(window_size),如 window_size=1 表示每个 token 只看自己和左右各1个 token。
- 窗口大小直接影响模型能看到的上下文范围,窗口越大,模型能利用的上下文信息越多,但计算量也随之增加。
- 适合局部上下文特征为主的任务,训练和推理速度较快。
- 不需要配置窗口,Transformer 通过自注意力机制自动建模全局上下文(只受最大 token 长度限制)。
- 每个 token 都能“看到”输入序列中的所有 token,能捕捉长距离依赖和复杂语义关系。
- 适合需要全局信息、长文本建模的场景,但对显存和计算资源要求更高。
实践建议:
- 如果你的分句任务主要依赖于局部标点和短距离上下文,CNN结构(如HashEmbedCNN)即可满足需求,且效率高。
- 如果文本结构复杂、长距离依赖明显,或希望获得更强的泛化能力,建议优先尝试Transformer结构。
🏁 总结与展望
通过本文,你已经掌握了 spaCy 中文分句的多种实现路径,从规则分句到可训练模型,从底层原理到工程实践,每一步都配有详实的代码和应用建议。你不仅能独立完成分句模型的训练、推理和评估,还能根据实际业务需求,灵活选择最合适的 pipeline 组件和底层结构,实现高效、智能的文本分句与分块。
未来,随着大模型和 RAG 技术的不断发展,分句与分块的边界将更加模糊,如何结合业务场景、数据特点和下游任务,设计出最优的文本预处理方案,将成为提升检索与生成效果的关键。希望本教程能成为你 NLP 实践路上的坚实基石,助你高效解决实际问题。