☰
基于BART的中文生成式摘要实战:从模型选型到部署优化
2026/10/20 13:19:28 网站建设 项目流程

1. 中文自动摘要为什么值得用BART来做

中文自动摘要这个任务,说简单也简单,说难也真难。简单在于,如果只是做抽取式摘要,把原文里权重最高的几个句子拼起来就能交差;难在于,真正有价值的是生成式摘要——模型要像人一样,读完一段长文之后用自己的话把核心信息重新组织出来。这个能力在新闻快讯、会议纪要、论文速览、客服工单归纳等场景里都是刚需。

我最早接触这个方向的时候,试过用Seq2Seq加注意力机制从零训练,结果惨不忍睹:生成的内容要么重复啰嗦,要么直接跑偏,训练成本还高得离谱。后来转向预训练模型微调的路子,才算是找到了正轨。在中文生成式摘要这个任务上,BART(Bidirectional and Auto-Regressive Transformers)架构是我实测下来性价比最高的选择之一。

BART的核心思路是“去噪自编码器”——它先把完整句子用各种方式打乱、遮盖、删除,然后让模型学会还原原始文本。这个预训练目标让它天然适合做摘要:编码器负责理解被“破坏”过的输入,解码器负责生成流畅的完整输出。相比GPT系列纯自回归的架构,BART在摘要任务上的表现通常更稳,因为它的编码器是双向的,对原文语义的把握更充分。

这篇文章适合谁看?如果你已经会用PyTorch搭基本的模型训练流程,想快速上手一个中文摘要的可用方案,那接下来的内容可以直接抄作业。如果你是完全的新手,也不用慌,我会把每个环节的“为什么”讲清楚,你跟着走一遍就能跑通。

提示:本文所有代码基于PyTorch和HuggingFace Transformers生态,建议使用带GPU的环境运行,CPU训练会慢到让你怀疑人生。

2. 整体方案设计与技术选型拆解

2.1 为什么选BART而不是T5或GPT

在中文生成式摘要这个任务上,常见的预训练模型选择主要有三条路线:BART系、T5系、GPT系。我分别试过之后,说说我的实际感受。

T5把一切任务都统一成“文本到文本”的格式,思路很优雅,但它在中文摘要上的表现对提示词(prefix)的设计比较敏感。你得花不少精力去调“summarize: ”这类前缀的写法,而且不同数据集上最优的prefix还不一样,调参成本偏高。

GPT系是纯解码器架构,生成能力很强,但它的编码器本质上就是解码器的一部分,对输入原文的双向理解不如BART充分。在长文本摘要场景下,GPT容易漏掉原文中后段的关键信息。

BART的编码器是双向Transformer,解码器是自回归Transformer,这个组合在摘要任务上几乎是量身定做的。编码器把原文的每个位置都看到,解码器再逐词生成摘要。而且BART的预训练目标里本身就包含“句子排列”和“文本填充”这类任务,跟摘要的“理解+重组”需求高度吻合。

2.2 中文预训练权重的选择

BART原始版本是在英文语料上训练的,直接拿来跑中文肯定不行。我们需要用中文BART预训练权重。目前社区里比较成熟的中文BART权重有几个来源,我实测下来,在中文摘要任务上表现比较稳的是基于大规模中文语料训练的那几个版本。

选权重的时候要注意几个点:第一,确认词表大小和分词器是否匹配,中文BART通常用字符级或BPE分词,词表大小在5万左右;第二,看预训练语料的领域覆盖,如果摘要任务偏向新闻,就选新闻语料占比高的权重;第三,模型参数量要和你的显存匹配,base版通常在2亿参数左右,large版会到4亿以上,显存不够的话训练时batch size只能压到很小。

我个人的建议是:先用base版跑通全流程,确认数据管线和训练逻辑没问题之后,再考虑换large版冲效果。一上来就上large版,很容易在数据预处理阶段就卡住。

2.3 数据处理管线的设计思路

中文摘要的数据集通常长这样:一条样本包含一篇原文(article)和一段参考摘要(summary)。我们需要把这两部分分别编码,原文喂给编码器,摘要喂给解码器(作为训练目标)。

这里有个关键细节:标签对齐。训练时,解码器的输入是“右移一位”的摘要,标签是原始摘要。HuggingFace的DataCollatorForSeq2Seq已经帮我们处理好了这个偏移逻辑,你只需要把原文和摘要分别tokenize,然后交给collator就行。

另一个容易踩坑的地方是最大长度设置。原文的max_length设多少?摘要的max_length设多少?设太小,原文信息被截断,摘要质量下降;设太大,显存爆炸,训练速度骤降。我的经验是:先统计你数据集里原文长度的分布,取95分位数作为max_length的参考值。中文摘要任务里,原文max_length设在512到1024之间比较常见,摘要max_length设在128到256之间通常够用。

2.4 评估指标的选择

摘要任务的评估指标主要有ROUGE系列(ROUGE-1、ROUGE-2、ROUGE-L)和BERTScore。ROUGE衡量的是n-gram重叠度,计算快、可解释性强,是学术界和工业界最常用的指标。BERTScore用预训练模型的嵌入来计算语义相似度,对同义替换更宽容,但计算成本高。

我的做法是:训练过程中用ROUGE-1和ROUGE-L做快速验证,看模型有没有在学;最终评估时再加上ROUGE-2和BERTScore,做更全面的判断。注意,ROUGE高不代表摘要读起来一定通顺,人工抽检永远不能省。

3. 核心细节解析与实操要点

3.1 环境准备与依赖安装

先把环境搭起来。我习惯用conda建一个独立环境,避免和系统里的其他包打架。

conda create -n bart_sum python=3.9 conda activate bart_sum pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers datasets evaluate rouge_score accelerate pip install jieba # 中文分词辅助,可选

这里有几个版本兼容性的坑要注意。transformers的版本建议在4.30以上,因为早期版本对中文BART的支持不够完善。datasets库用来加载和处理数据,evaluate库用来算ROUGE。如果你用的是较新的PyTorch,accelerate库能帮你自动处理混合精度和分布式训练,省不少事。

注意:不要同时装rouge和rouge_score两个包,它们会冲突。用rouge_score就够了,evaluate库底层调的就是它。

3.2 模型与分词器的加载

加载中文BART模型和对应的分词器,代码很简洁:

from transformers import BartForConditionalGeneration, BertTokenizer model_name = "your-chinese-bart-checkpoint" # 替换为实际的中文BART权重路径 tokenizer = BertTokenizer.from_pretrained(model_name) model = BartForConditionalGeneration.from_pretrained(model_name)

这里有个细节:中文BART的分词器通常沿用BERT的字符级分词方案,所以用BertTokenizer加载是合理的。如果你用的权重自带分词器配置,直接用AutoTokenizer也行。

加载之后,检查一下词表大小和模型配置:

print(tokenizer.vocab_size) print(model.config.vocab_size) print(model.config.max_position_embeddings)

这三个值要匹配。如果vocab_size对不上,说明权重和分词器不配套,训练时embedding层会报错。max_position_embeddings决定了模型能处理的最大序列长度,中文BART通常是1024,如果你的原文超过这个长度,要么截断,要么换支持更长序列的权重。

3.3 数据预处理的关键步骤

假设你的数据是JSON格式,每条包含article和summary两个字段。预处理流程如下:

def preprocess_function(examples): inputs = examples["article"] targets = examples["summary"] model_inputs = tokenizer( inputs, max_length=512, truncation=True, padding="max_length" ) labels = tokenizer( targets, max_length=128, truncation=True, padding="max_length" ) model_inputs["labels"] = labels["input_ids"] return model_inputs

这段代码看起来简单,但有几个坑我踩过。第一,padding="max_length"在训练时会让所有样本对齐到同一长度,方便批处理,但会浪费显存。如果显存紧张,可以改成padding=True,让collator动态padding。第二,labels里的padding token会被损失函数忽略吗?默认情况下,HuggingFace的模型会把label为-100的位置忽略掉,但这里我们直接用了input_ids,padding位置的值是tokenizer的pad_token_id,不是-100。所以需要手动处理:

labels["input_ids"] = [ [(l if l != tokenizer.pad_token_id else -100) for l in label] for label in labels["input_ids"] ]

这个细节不注意的话,模型会去学预测padding token,损失曲线看起来在降,但实际生成质量很差。

3.4 训练参数的选择逻辑

训练参数直接决定模型能不能收敛、收敛得好不好。我通常从以下配置起步:

training_args = Seq2SeqTrainingArguments( output_dir="./bart_sum_output", evaluation_strategy="epoch", learning_rate=3e-5, per_device_train_batch_size=8, per_device_eval_batch_size=8, weight_decay=0.01, save_total_limit=3, num_train_epochs=5, predict_with_generate=True, fp16=True, logging_steps=100, warmup_ratio=0.1, )

逐个解释一下这些参数背后的考量。learning_rate=3e-5是预训练模型微调的经典起点,太大容易破坏预训练学到的知识,太小收敛太慢。per_device_train_batch_size=8是在16G显存下跑base版模型的经验值,如果你用large版或者序列更长,得往下调。fp16=True开启混合精度训练,能省将近一半显存,速度也有提升,但要注意有些操作在fp16下会溢出,如果loss出现NaN,先关掉fp16试试。warmup_ratio=0.1让学习率在前10%的步数里线性上升,避免训练初期梯度太大把模型带偏。

predict_with_generate=True这个参数很关键,它让模型在评估时用生成的方式输出摘要,而不是用teacher forcing。这样算出来的ROUGE才是真实的生成质量。

3.5 生成阶段的关键参数

训练完之后,用模型生成摘要时,有几个解码参数需要调:

summary_ids = model.generate( input_ids, max_length=128, min_length=20, num_beams=4, length_penalty=2.0, early_stopping=True, no_repeat_ngram_size=3 )

num_beams=4是beam search的宽度,越大生成质量通常越好,但速度越慢。length_penalty=2.0对长摘要更友好,因为beam search倾向于生成短序列,这个惩罚项能平衡长度。no_repeat_ngram_size=3防止生成重复的3-gram,中文摘要里模型很容易陷入“的的的”或者重复短语的循环,这个参数能有效缓解。min_length=20避免生成过短的摘要,有些模型会偷懒只输出几个词。

这些参数没有绝对的最优值,需要根据你的数据和任务特点去试。我的习惯是固定其他参数,每次只调一个,观察ROUGE的变化。

4. 完整实操流程与核心环节实现

4.1 数据加载与划分

假设你有一个中文摘要数据集,存成JSON Lines格式,每行一个样本。加载和划分的代码如下:

from datasets import load_dataset dataset = load_dataset("json", data_files={ "train": "data/train.jsonl", "validation": "data/val.jsonl", "test": "data/test.jsonl" }) print(dataset)

如果数据量不大,也可以手动用train_test_split划分。注意验证集和测试集要分开,验证集用来调参和早停,测试集只在最后评估时用一次。我见过有人把测试集当验证集反复调参,最后报出来的指标虚高,实际部署时效果差一大截。

数据划分比例通常是8:1:1或者7:1:2。如果数据量少于1万条,建议用交叉验证,否则验证集的波动会很大,你根本分不清模型是真的进步了还是运气好。

4.2 数据预处理与Dataset构建

用dataset.map把预处理函数应用到所有样本:

tokenized_dataset = dataset.map( preprocess_function, batched=True, remove_columns=dataset["train"].column_names )

batched=True让map函数一次处理一批样本,速度比逐条处理快很多。remove_columns把原始文本列删掉,只保留tokenize后的输入,减少内存占用。

然后设置训练格式:

tokenized_dataset.set_format( type="torch", columns=["input_ids", "attention_mask", "labels"] )

这里attention_mask是必须的,它告诉模型哪些位置是真实token,哪些是padding。没有它,模型会把padding也当成有效输入,注意力机制会被干扰。

4.3 数据整理器与批处理

DataCollatorForSeq2Seq负责把不同长度的样本整理成统一batch:

from transformers import DataCollatorForSeq2Seq data_collator = DataCollatorForSeq2Seq( tokenizer=tokenizer, model=model, padding=True, max_length=512 )

这个collator会自动处理labels的偏移和padding,比手动写省心得多。注意padding=True表示动态padding到当前batch的最大长度,而不是全局最大长度,能省显存。

4.4 训练器配置与启动

把模型、数据、参数、collator组装到Trainer里:

from transformers import Seq2SeqTrainer trainer = Seq2SeqTrainer( model=model, args=training_args, train_dataset=tokenized_dataset["train"], eval_dataset=tokenized_dataset["validation"], tokenizer=tokenizer, data_collator=data_collator, compute_metrics=compute_metrics, ) trainer.train()

compute_metrics函数用来在评估时计算ROUGE:

import evaluate import numpy as np rouge = evaluate.load("rouge") def compute_metrics(eval_pred): predictions, labels = eval_pred decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True) labels = np.where(labels != -100, labels, tokenizer.pad_token_id) decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) result = rouge.compute( predictions=decoded_preds, references=decoded_labels, use_stemmer=True ) return {k: round(v, 4) for k, v in result.items()}

注意labels里-100的位置要替换成pad_token_id再解码,否则会报错。skip_special_tokens=True把特殊token去掉,让ROUGE计算更准确。

4.5 训练过程监控与早停

训练启动后,重点盯两个东西:loss曲线和ROUGE曲线。正常情况下,训练loss应该稳步下降,验证loss先降后升(过拟合的信号)。如果训练loss不降,检查学习率是不是太小、数据有没有问题、模型有没有正确加载预训练权重。如果验证loss很早就开始上升,说明过拟合了,减少epoch数或者加dropout。

早停策略我通常这样设:

from transformers import EarlyStoppingCallback trainer.add_callback(EarlyStoppingCallback( early_stopping_patience=2, early_stopping_threshold=0.001 ))

patience=2表示验证指标连续2个epoch没有提升就停。threshold=0.001表示提升小于0.001不算提升,避免被微小波动干扰。

4.6 模型保存与推理部署

训练完成后,保存模型和分词器:

trainer.save_model("./final_model") tokenizer.save_pretrained("./final_model")

推理时加载模型:

from transformers import pipeline summarizer = pipeline( "summarization", model="./final_model", tokenizer="./final_model", device=0 # 用GPU ) article = "你的中文长文本..." result = summarizer(article, max_length=128, min_length=20, do_sample=False) print(result[0]["summary_text"])

device=0指定用第一块GPU,do_sample=False用beam search而不是随机采样,保证输出稳定。如果要做批量推理,建议手动写循环而不是用pipeline,因为pipeline的批处理效率不高。

4.7 推理性能优化

实际部署时,推理速度很重要。几个优化手段:第一,用torch.no_grad()包裹推理代码,关掉梯度计算;第二,把模型设为eval模式,关掉dropout;第三,如果延迟要求高,可以考虑用ONNX Runtime或者TensorRT做推理加速,但转换过程有坑,建议先确保PyTorch版本跑通再折腾。

model.eval() with torch.no_grad(): outputs = model.generate(input_ids, ...)

另外,如果输入文本特别长,可以考虑先做段落切分,分别摘要后再合并,避免超出模型的最大位置编码限制。

5. 常见问题与排查技巧实录

5.1 训练loss不下降怎么办

这是最常见的问题。排查顺序如下:先确认预训练权重是否正确加载,打印模型第一层embedding的权重,看看是不是随机初始化的;再检查数据预处理,把tokenize后的input_ids解码回文本,看看是不是乱码或者被截断得只剩几个字;然后看学习率,3e-5不行就试1e-4或者5e-5;最后检查labels的-100替换有没有做,如果没做,模型在学padding,loss会降但生成质量极差。

5.2 生成的摘要重复啰嗦

中文BART在生成时容易陷入重复循环,比如反复输出“他表示他表示他表示”。解决方法:开启no_repeat_ngram_size=3,加repetition_penalty=1.2,调大length_penalty。如果还不行,说明模型过拟合了,减少训练epoch或者增加训练数据。

5.3 显存不够用

显存不够的解决路径:先降batch size,从8降到4再降到2;然后开fp16或者bf16;再然后缩短max_length,原文从1024降到512;还不行就换base版模型,别用large版;最后考虑用gradient accumulation,用时间换空间。

training_args = Seq2SeqTrainingArguments( per_device_train_batch_size=4, gradient_accumulation_steps=4, # 等效batch size = 16 ... )

5.4 ROUGE分数虚高但摘要读不通

这种情况通常是数据泄露或者评估方式有问题。检查验证集和训练集有没有重叠样本;检查compute_metrics里解码时有没有把padding也算进去;检查参考摘要是不是太短,导致模型只要输出几个高频词就能拿到高ROUGE。人工抽检20条生成结果,如果读起来不通顺,ROUGE再高也没用。

5.5 中文分词对摘要质量的影响

中文BART通常用字符级分词,不依赖jieba等分词工具。但如果你在预处理阶段做了分词,再喂给字符级分词器,会导致token序列变长、语义碎片化。我的建议是:直接用原始中文字符串,不要预先分词,让模型的分词器自己处理。

5.6 常见问题速查表

问题现象可能原因排查方法解决方案
loss不下降权重未加载/学习率过小/labels未处理打印权重、检查数据、查看labels重新加载权重、调大学习率、替换-100
生成重复过拟合/解码参数不当查看训练epoch数加no_repeat_ngram_size、减epoch
显存溢出batch过大/序列过长监控显存占用降batch、开fp16、缩短序列
ROUGE虚高数据泄露/评估错误检查数据划分、解码逻辑重新划分数据、修正解码
摘要不通顺训练不足/过拟合人工抽检调参、加数据、早停

6. 我踩过的坑和实操心得

第一个坑:一开始我没注意labels的-100替换,训练loss降得很漂亮,但生成的摘要全是“的的的的的”。后来打印解码结果才发现,模型在学预测padding token。这个细节HuggingFace文档里提了,但很容易被忽略。

第二个坑:max_length设得太大。我一开始把原文max_length设成1024,结果batch size只能开到2,训练一个epoch要几个小时。后来统计了数据长度分布,发现95%的原文都在400字以内,把max_length降到512之后,batch size开到16,训练速度翻了将近8倍,效果还没降。

第三个坑:beam search的num_beams设成10。生成质量确实好了一点,但推理速度慢了5倍,线上根本扛不住。后来降到4,质量和速度的平衡点就找到了。

第四个坑:用CPU做推理。测试阶段我用CPU跑了几条,每条要等十几秒,完全没法用。换成GPU之后,每条不到0.5秒。如果你没有GPU,建议至少用ONNX Runtime做CPU加速,或者考虑用更小的模型。

第五个坑:忽略验证集的分布。有一次我的验证集里全是短摘要,模型学会了输出短文本,ROUGE很高,但测试集里长摘要居多,生成结果被截断得厉害。后来我确保验证集和测试集的长度分布一致,指标才可信。

最后分享一个小技巧:训练中文BART做摘要时,可以在输入原文前面加一个前缀,比如“摘要:”,让模型更明确任务类型。这个技巧在T5上更常用,但在BART上也有一定效果,尤其是当你用同一个模型做多个生成任务时。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询