中文自动摘要这个任务,说简单也简单,说坑也多。简单在于,如果你只是拿个预训练模型跑个demo,几行代码就能出结果;坑多在于,当你真正想把它用到业务里,会发现生成的结果要么重复啰嗦,要么漏掉关键信息,要么干脆把原文抄了一遍。我用BART做中文摘要有一段时间了,从最初的直接调用预训练权重,到后来自己整理数据做微调,中间踩了不少坑,也积累了一些实操经验。这篇文章就把整个流程拆开揉碎讲清楚,从模型选型、数据准备、训练配置到推理优化,每一步都给出可复现的方案和参数依据。适合有一定PyTorch基础、想快速上手中文摘要的开发者,也适合已经在做摘要但效果不理想的同学参考。
1. 中文摘要任务的整体设计与模型选型
1.1 为什么选BART而不是T5或GPT
做中文摘要,模型架构的选择直接决定了后续的训练成本和生成质量。目前主流方案有三类:以BART为代表的去噪自编码器、以T5为代表的文本到文本统一框架、以及以GPT为代表的纯解码器架构。我最初也纠结过选哪个,后来通过对比实验发现,BART在摘要任务上的表现确实更稳。
原因在于BART的预训练方式。它采用“破坏文本再重建”的策略,具体来说是把原文用多种方式打乱(比如随机遮盖、句子重排、文本填充),然后让模型学会恢复原始文本。这种训练目标天然适合摘要任务,因为摘要本质上就是从长文本中提取核心信息并重新组织,和BART的预训练目标高度一致。T5虽然也是编码器-解码器结构,但它的预训练任务更偏向多任务混合,在中文摘要上的收敛速度往往不如BART。GPT系列是纯解码器,生成流畅度好,但容易偏离原文事实,做摘要时幻觉问题比较明显。
具体到中文场景,我推荐使用fnlp/bart-base-chinese这个预训练权重。它是基于中文语料训练的BART模型,词表覆盖了常见汉字和词组,编码器6层、解码器6层,隐藏层维度768,注意力头数12,总参数量约1.4亿。这个规模在单张消费级显卡上就能微调,推理速度也够用。如果你追求更好的效果,可以上bart-large-chinese,参数量翻倍到4亿左右,但显存需求会明显增加。
提示:不要直接用英文BART权重做中文摘要,词表不匹配会导致大量未知token,生成结果基本不可用。
1.2 摘要任务的核心难点与应对思路
中文摘要的难点和英文不太一样。英文有空格分词,中文需要先做分词或依赖模型自身的tokenizer。BART的中文tokenizer是基于BPE的,能较好地处理未登录词,但在处理数字、专有名词时仍可能出现切分错误。我遇到过把“2023年”切成“202”“3”“年”的情况,虽然不影响理解,但生成时容易丢字。
另一个难点是长文本处理。BART的位置编码最大长度通常是1024,但很多新闻或报告远超这个长度。直接截断会丢失关键信息,我的做法是先用TextRank或Lead-3提取关键句,把原文压缩到800字以内再送入模型。这样既保留了核心信息,又避免了截断带来的信息损失。
还有一个容易被忽视的问题是重复生成。BART在解码时如果没加约束,容易反复输出同一句话。解决办法是在生成时启用no_repeat_ngram_size参数,通常设为3,即禁止3-gram重复。同时配合repetition_penalty设为1.2左右,能有效抑制重复。
1.3 整体流程设计
整个中文摘要系统的流程可以拆成四步:数据准备、模型微调、推理生成、后处理。数据准备阶段需要收集原文-摘要对,清洗格式,划分训练集和验证集。模型微调阶段加载预训练权重,设置训练参数,监控损失和ROUGE指标。推理生成阶段用beam search或采样策略生成摘要,控制长度和重复。后处理阶段做去重、截断、标点修正。
这个流程看起来线性,但实际操作中需要反复迭代。比如训练时发现验证集ROUGE不涨,可能要回头检查数据质量;推理时发现生成结果太短,可能要调整min_length和max_length的比例。我建议先把流程跑通,再逐步调优每个环节。
2. 数据准备与预处理的关键细节
2.1 中文摘要数据集的获取与清洗
公开的中文摘要数据集不多,常用的有LCSTS(中文微博摘要)、NLPCC摘要任务数据、以及一些新闻摘要数据集。LCSTS有约240万条微博原文-摘要对,但质量参差不齐,很多摘要就是原文的前几个字。NLPCC的数据质量更高,但规模较小,约5万条。我通常把LCSTS做预训练,再用NLPCC做精调。
清洗数据时要注意几个点。第一,过滤掉摘要长度小于10个字的样本,这类摘要信息量太少,训练时容易让模型学会“偷懒”。第二,过滤掉原文和摘要完全相同的样本,这是数据标注错误。第三,检查特殊字符,比如HTML标签、乱码、多余空格,用正则统一清理。第四,对原文做长度过滤,保留50到1000字之间的样本,太短的没必要摘要,太长的截断或分段处理。
import re def clean_text(text): text = re.sub(r'<[^>]+>', '', text) # 去HTML标签 text = re.sub(r'\s+', ' ', text) # 合并空白 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:""''()]', '', text) return text.strip() def filter_sample(example): src = clean_text(example['text']) tgt = clean_text(example['summary']) if len(tgt) < 10 or len(src) < 50: return False if src == tgt: return False return True2.2 数据格式转换与Tokenization
BART的输入需要转成token id序列。用HuggingFace的BartTokenizer时,注意设置max_source_length和max_target_length。我通常把原文截断到512个token,摘要截断到128个token。这个比例是根据中文摘要的统计规律定的:摘要长度一般是原文的10%到20%,512的20%约100,留些余量到128。
Tokenization时有个细节:BART的tokenizer默认会在序列末尾加</s>,这是解码器的起始符。编码器输入不需要这个,所以要对原文和摘要分别处理。原文用tokenizer(text, max_length=512, truncation=True, padding='max_length'),摘要用tokenizer(summary, max_length=128, truncation=True, padding='max_length')。注意摘要的labels要把padding位置的token id设为-100,这样损失计算时会忽略这些位置。
from transformers import BartTokenizer tokenizer = BartTokenizer.from_pretrained('fnlp/bart-base-chinese') def preprocess(example): inputs = tokenizer( example['text'], max_length=512, truncation=True, padding='max_length', return_tensors='pt' ) with tokenizer.as_target_tokenizer(): labels = tokenizer( example['summary'], max_length=128, truncation=True, padding='max_length', return_tensors='pt' ) labels['input_ids'][labels['input_ids'] == tokenizer.pad_token_id] = -100 return { 'input_ids': inputs['input_ids'].squeeze(), 'attention_mask': inputs['attention_mask'].squeeze(), 'labels': labels['input_ids'].squeeze() }2.3 数据划分与类别平衡
训练集、验证集、测试集的比例我一般按8:1:1划分。如果数据量超过10万,可以按98:1:1,验证集不用太大,几千条足够监控过拟合。划分时要保证随机性,用train_test_split设置random_state固定种子,方便复现。
类别平衡方面,中文摘要数据往往存在主题分布不均的问题,比如娱乐新闻远多于科技新闻。如果业务场景对某些领域有偏好,可以在训练时对样本加权,或者对少数领域做数据增强。我试过用同义词替换和句子重排做增强,但效果有限,不如直接多收集一些目标领域的数据。
注意:验证集和测试集必须来自同一分布,否则ROUGE指标会虚高。我见过有人用微博数据训练,用新闻数据测试,结果ROUGE-1只有0.2出头,完全没法用。
3. 模型微调与训练配置实战
3.1 训练环境与超参数设置
微调BART-base-chinese,单张12GB显存的显卡就够。batch size设8,梯度累积步数设4,等效batch size为32。学习率用3e-5,这是BART微调的常用值,太大容易震荡,太小收敛慢。优化器选AdamW,权重衰减0.01。学习率调度用线性预热,预热步数占总步数的10%。
训练轮数一般3到5轮就够了。我试过训10轮,第4轮之后验证集ROUGE就开始下降,明显过拟合。早停策略设patience=2,即验证集指标连续2轮不提升就停止。模型保存时只保留验证集ROUGE最高的checkpoint,不要保存最后一轮的,因为最后一轮往往过拟合。
from transformers import BartForConditionalGeneration, AdamW, get_linear_schedule_with_warmup model = BartForConditionalGeneration.from_pretrained('fnlp/bart-base-chinese') optimizer = AdamW(model.parameters(), lr=3e-5, weight_decay=0.01) total_steps = len(train_loader) * 5 // 4 # 梯度累积 scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=int(0.1 * total_steps), num_training_steps=total_steps )3.2 训练过程中的监控与调优
训练时要监控三个指标:训练损失、验证损失、验证集ROUGE。训练损失下降但验证损失上升,说明过拟合,要早停或加正则。验证损失下降但ROUGE不涨,可能是生成策略有问题,比如beam search的beam数太小。
我习惯每500步评估一次,用generate方法生成验证集的摘要,计算ROUGE-1、ROUGE-2、ROUGE-L。ROUGE-1反映词汇重叠,ROUGE-2反映短语连贯性,ROUGE-L反映句子结构。中文摘要通常ROUGE-1在0.4左右,ROUGE-2在0.2左右,ROUGE-L在0.35左右算不错。如果ROUGE-1超过0.5,要警惕是不是数据泄漏了。
生成时用num_beams=4,length_penalty=2.0,no_repeat_ngram_size=3。length_penalty大于1会鼓励生成更长的摘要,中文摘要通常需要这个,因为模型倾向于生成短句。early_stopping=True让beam search在找到完整候选后提前结束,加速推理。
3.3 显存优化与混合精度训练
如果显存不够,可以开启混合精度训练。用torch.cuda.amp的autocast和GradScaler,显存占用能降低30%左右,训练速度提升20%。注意混合精度下损失缩放要正确,GradScaler会自动处理。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for batch in train_loader: optimizer.zero_grad() with autocast(): outputs = model(**batch) loss = outputs.loss scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()如果还是不够,可以减小batch size到4,梯度累积步数加到8。或者用gradient_checkpointing,用时间换空间,显存能再降40%,但训练速度会慢一些。我一般优先用混合精度,实在不行再上gradient checkpointing。
提示:混合精度训练时,
labels里的-100不会被自动转换,需要确保它们保持为整数类型,否则损失计算会出错。
4. 推理生成与后处理优化
4.1 生成策略的选择与参数调优
推理阶段的核心是生成策略。贪心搜索速度快但容易重复,beam search质量好但慢。我通常用beam search,beam数设4到6。beam数太大收益递减,8以上基本没提升,反而显存翻倍。
关键参数有三个:max_length、min_length、length_penalty。max_length设128,min_length设30,防止生成过短。length_penalty设2.0,鼓励生成更完整的摘要。no_repeat_ngram_size设3,禁止3-gram重复。repetition_penalty设1.2,进一步抑制重复。
def generate_summary(text): inputs = tokenizer(text, max_length=512, truncation=True, return_tensors='pt') with torch.no_grad(): summary_ids = model.generate( inputs['input_ids'].to(device), attention_mask=inputs['attention_mask'].to(device), max_length=128, min_length=30, num_beams=4, length_penalty=2.0, no_repeat_ngram_size=3, repetition_penalty=1.2, early_stopping=True ) return tokenizer.decode(summary_ids[0], skip_special_tokens=True)4.2 后处理:去重、截断与标点修正
模型生成的摘要常有重复词和多余标点。我写了一个后处理函数,先用正则去掉连续重复的短语,再修正常见标点错误,比如连续逗号、句末缺句号。如果摘要超过指定长度,按句号截断,保证语义完整。
def postprocess(summary): summary = re.sub(r'(.{2,}?)\1+', r'\1', summary) # 去连续重复 summary = re.sub(r'[,,]{2,}', ',', summary) # 合并逗号 summary = re.sub(r'[。.]{2,}', '。', summary) # 合并句号 if not summary.endswith(('。', '!', '?')): summary += '。' return summary4.3 长文本摘要的分段与融合
超过512字的原文,我采用分段摘要再融合的策略。先用TextRank提取关键句,把原文压缩到800字以内,再按512字切分,每段生成摘要,最后把各段摘要拼接,再用一次BART做摘要融合。这样能处理任意长度的文本,但计算量会翻倍。
TextRank的实现可以用jieba.analyse.textrank,提取前20个关键句。注意关键句要按原文顺序排列,不能打乱,否则摘要逻辑会乱。融合阶段把各段摘要拼成一段,再送入模型生成最终摘要,max_length设150,min_length设50。
注意:分段摘要时,段与段之间要有重叠,通常重叠50字,避免关键信息被切断。重叠部分在融合时会自然去重。
5. 常见问题排查与避坑经验
5.1 生成结果重复或循环
这是最常见的问题。原因通常是解码时没有加重复惩罚,或者训练数据里有大量重复样本。解决办法:训练时过滤重复样本,推理时加no_repeat_ngram_size=3和repetition_penalty=1.2。如果还不行,试试num_beams=1用贪心搜索,有时beam search反而会陷入循环。
我遇到过一次,模型反复输出“的的的的”,查了半天发现是tokenizer的pad_token和eos_token设成了同一个,导致解码时无法正确终止。检查tokenizer配置,确保pad_token_id、eos_token_id、decoder_start_token_id互不相同。
5.2 ROUGE指标不涨或下降
训练几轮后ROUGE不涨,先检查数据质量。用pandas抽样看几条原文和摘要,确认摘要不是原文的截断。再检查tokenization,看原文和摘要的token长度分布,如果摘要平均长度只有20个token,说明摘要太短,模型学不到东西。
如果数据没问题,调学习率。3e-5不行就试1e-5,再不行试5e-5。学习率太大导致震荡,太小导致欠拟合。还可以试不同的length_penalty,1.0到2.5之间调,看哪个ROUGE最高。
5.3 显存溢出与训练中断
显存溢出通常发生在batch size太大或序列太长时。解决办法:减小batch size,缩短max_source_length,开启混合精度,用gradient checkpointing。如果训练中途中断,检查是不是某个batch的序列特别长,可以用DataLoader的collate_fn做动态padding,按batch内最长序列padding,而不是全局padding到512。
def collate_fn(batch): input_ids = [item['input_ids'] for item in batch] labels = [item['labels'] for item in batch] input_ids = torch.nn.utils.rnn.pad_sequence(input_ids, batch_first=True, padding_value=tokenizer.pad_token_id) labels = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True, padding_value=-100) attention_mask = (input_ids != tokenizer.pad_token_id).long() return {'input_ids': input_ids, 'attention_mask': attention_mask, 'labels': labels}5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 生成结果重复 | 无重复惩罚 | 加no_repeat_ngram_size=3和repetition_penalty=1.2 |
| ROUGE不涨 | 学习率不当 | 试1e-5、3e-5、5e-5 |
| 显存溢出 | batch太大 | 减小batch,开混合精度 |
| 摘要太短 | min_length太小 | 设min_length=30 |
| 摘要偏离原文 | 幻觉 | 降低temperature,用beam search |
| 训练不收敛 | 数据噪声 | 清洗数据,过滤短摘要 |
| 推理速度慢 | beam数太大 | 减到4,或改用贪心 |
| 标点混乱 | 后处理缺失 | 加正则修正 |
6. 效果评估与迭代方向
6.1 自动评估与人工评估结合
ROUGE是自动评估的标配,但它只看词汇重叠,不看语义。我通常ROUGE和人工评估一起做。人工评估抽100条,从流畅性、信息量、事实一致性三个维度打分,每个维度1到5分。流畅性看句子通顺与否,信息量看是否覆盖原文要点,事实一致性看有没有编造内容。
如果ROUGE高但人工评分低,说明模型在“抄”原文,没有真正理解。这时候要检查训练数据,看摘要是不是原文的简单截断。如果ROUGE低但人工评分高,说明模型生成的摘要用词不同但语义正确,这种情况可以接受,甚至更好。
6.2 领域适配与持续学习
通用摘要模型在特定领域往往表现不佳。比如医疗摘要需要保留专业术语,法律摘要需要精确引用条款。我的做法是在通用模型基础上,用领域数据做继续预训练,然后再微调摘要任务。领域数据不需要标注摘要,只要原文就行,用BART的去噪目标做自监督训练。
持续学习方面,如果业务数据不断更新,可以定期用新数据微调模型,但要注意灾难性遗忘。我通常保留10%的通用数据混在领域数据里一起训练,这样模型不会忘记通用能力。
6.3 模型压缩与部署优化
如果要在边缘设备部署,模型压缩是必须的。我试过用torch.quantization做动态量化,模型大小从500MB降到150MB,推理速度提升2倍,ROUGE只降了0.02。还可以用ONNX导出,再用ONNX Runtime推理,速度比PyTorch快30%左右。
import torch.quantization model.eval() quantized_model = torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtype=torch.qint8 ) torch.save(quantized_model.state_dict(), 'bart_quantized.pt')部署时用FastAPI包一个HTTP接口,输入原文,输出摘要。注意加超时和限流,防止长文本把服务拖垮。我一般设超时10秒,超过就返回“摘要生成超时,请缩短输入”。
提示:量化后的模型在CPU上推理更快,但GPU上可能没优势,因为GPU对int8的支持不如CPU成熟。部署前先测一下目标硬件的实际速度。
7. 实操心得与经验总结
7.1 数据质量决定上限
我做过对比实验,同样的模型和参数,用清洗过的数据比用原始数据,ROUGE-1能差0.05以上。数据清洗的投入绝对值得。具体来说,过滤短摘要、去重、修正标点、统一全半角,这些操作花不了多少时间,但效果立竿见影。
还有一个经验:摘要长度分布要均匀。如果训练数据里摘要都是20字左右,模型就只会生成20字的摘要。我通常把摘要按长度分桶,每个桶的样本数差不多,这样模型能学会生成不同长度的摘要。
7.2 训练参数不要照搬
网上的教程往往给一套参数就完事,但实际数据不同,最优参数也不同。学习率、batch size、beam数这些都要根据验证集表现调。我一般先跑一个小规模实验,用1万条数据训2轮,快速试几组参数,找到大致范围后再全量训练。
length_penalty这个参数特别敏感。中文摘要通常需要1.5到2.5之间,英文摘要1.0到1.5就够了。我试过设3.0,结果生成了一堆废话,把原文的细节都抄进去了。
7.3 推理速度与质量的权衡
beam search的beam数从4加到8,ROUGE可能只涨0.01,但推理时间翻倍。如果业务对延迟敏感,beam数设2或3就够了,或者用贪心搜索加重复惩罚。我实测下来,beam=3在质量和速度之间平衡最好。
还有一个技巧:用torch.no_grad()包裹推理代码,能省不少显存。如果批量推理,把多条原文拼成一个batch,比逐条推理快很多。但要注意padding,不同长度的原文padding到同一长度会浪费计算,用动态padding按batch内最长序列padding。
7.4 后处理不能省
模型生成的摘要直接输出往往不能看,标点混乱、重复词、缺句号都是常事。后处理函数虽然简单,但能显著提升可读性。我建议把后处理做成可配置的,不同业务场景用不同的规则。比如新闻摘要需要保留数字和日期,社交摘要可以去掉表情符号。
最后分享一个小技巧:如果生成的摘要开头总是“本文介绍了”或“据悉”,可以在后处理里用正则去掉这些模板化开头。这些词往往是训练数据里的高频模式,模型学会了偷懒。去掉之后摘要更简洁,信息密度更高。
这个方案后续还可以扩展的方向包括:多文档摘要(把多篇相关文章合成一篇摘要)、可控摘要(指定摘要长度或关键词)、跨语言摘要(中文原文生成英文摘要)。每个方向都有不同的技术挑战,但核心流程和BART的使用方法是相通的。