
中文自动摘要这个任务说简单也简单说坑也多。简单在于如果你只是拿现成的预训练模型跑个demo几行代码就能出结果坑在于当你真正想把它用到自己的业务数据上会发现生成的结果要么重复啰嗦要么漏掉关键信息要么干脆把原文抄了一遍。我用BART做中文摘要有一段时间了从最初的“跑通就行”到后来反复调参、换分词器、改解码策略踩过的坑足够写一篇完整的复盘。这篇文章不打算复述官方文档而是把我实际项目中积累的经验、遇到的意外情况、以及那些文档里不会写的细节尽可能完整地分享出来。无论你是刚接触摘要任务的新手还是已经跑过几个模型但效果不理想的开发者应该都能从中找到一些可以直接用的东西。1. 为什么中文摘要值得单独拿出来做1.1 中文摘要和英文摘要的本质差异很多人觉得摘要任务就是“输入长文本、输出短文本”语言差异不大。但实际做下来中文摘要的难度在某些维度上明显高于英文。最直接的一点是分词。英文以空格自然分隔单词BART原版使用的分词器可以直接按子词切分语义单元相对清晰。中文没有空格虽然现在普遍用BPE或WordPiece做子词切分但中文的“词”边界本身就模糊同一个字符串在不同语境下可能是不同的词。这就导致模型在编码阶段就可能把语义单元切碎解码时再拼回来信息损失比英文更严重。另一个差异是中文的句子结构更灵活。英文摘要模型学到的往往是“主谓宾”的压缩模式但中文里省略主语、流水句、并列短句非常多。我试过直接用英文摘要的思路去处理中文新闻结果模型生成的摘要经常出现主语缺失、指代不明的问题。比如原文讲“某公司发布了新产品该产品支持多种功能”模型可能压缩成“某公司发布新产品支持多种功能”看起来没问题但如果是多主体交替出现的文本指代就会乱。还有一个容易被忽略的点中文的标点符号和英文不同。BART原版的分词器对中文标点的处理并不理想句号、逗号、顿号、分号在中文里的语义权重不一样但模型可能把它们统一当作分隔符。我在早期实验中就发现模型经常把顿号连接的两个并列项合并成一个导致摘要信息失真。所以做中文摘要第一步不是选模型而是想清楚你的文本特点和摘要需求。1.2 BART在中文摘要任务中的定位BART是一种去噪自编码器核心思想是先把输入文本加噪比如随机遮盖、打乱顺序、删除片段然后让模型学习恢复原始文本。这种预训练方式让它天生适合做序列到序列的生成任务摘要就是其中之一。相比T5、Pegasus等模型BART在摘要任务上的表现一直很稳尤其是它的编码器-解码器结构对长文本的压缩能力比较强。但要注意原版BART是在英文语料上预训练的直接拿来处理中文效果会很差。所以实际使用中我们用的是中文BART预训练模型比如在中文大规模语料上继续预训练的版本。这些模型已经学到了中文的语法和语义表示我们只需要在摘要数据集上做微调就能得到不错的效果。我个人的经验是如果你的摘要任务偏向新闻标题生成或短摘要中文BART微调后BLEU和ROUGE都能达到可用水平但如果要做长摘要或面向特定领域的摘要还需要额外的策略调整。1.3 摘要任务的两种主流范式做摘要之前先要明确你要的是抽取式还是生成式。抽取式摘要从原文中直接挑选重要句子拼成摘要优点是忠实原文、不会产生事实错误缺点是句子之间的连贯性差而且受限于原文表述。生成式摘要则让模型自己组织语言优点是流畅、压缩率高缺点是可能产生幻觉编造原文没有的信息。BART属于生成式摘要模型。我在实际项目中一般会根据场景选择如果是新闻快讯、财报摘要这类对事实准确性要求极高的场景我会用抽取式做初筛再用BART做润色和压缩如果是内容推荐、文章导读这类容错率较高的场景直接上BART生成即可。这个选择没有绝对的对错关键看你的业务能承受多大的事实偏差。2. 数据准备中文摘要最容易被低估的环节2.1 数据集的选择与清洗中文摘要的公开数据集不算多常用的有LCSTS中文微博新闻摘要、NLPCC摘要任务数据、以及一些新闻语料构建的数据集。LCSTS是比较经典的一个包含约240万条微博新闻和对应摘要但它的摘要普遍很短很多只有一句话适合做短摘要训练。如果你的目标是长摘要LCSTS可能不够用。我自己的做法是先根据业务场景确定摘要长度分布然后从公开数据集中筛选长度匹配的样本再补充自己的业务数据。清洗环节有几个必须做的操作第一去掉摘要和原文完全相同的样本这种数据会让模型学会“抄原文”第二去掉摘要长度超过原文的样本这通常是数据标注错误第三检查特殊字符和乱码中文文本里经常混入全角半角符号、HTML标签残留、表情符号等这些都会干扰分词器。还有一个细节中文摘要数据里经常出现“原标题”和“正文”混在一起的情况。有些数据集把新闻标题当作摘要但标题和摘要的写作风格差异很大。标题追求吸引眼球常用疑问句、感叹句摘要追求信息完整通常是陈述句。如果你用标题数据训练摘要模型生成的摘要会偏向标题风格读起来很跳。我一般会把标题和摘要分开处理或者至少在训练时给模型一个标识让它知道当前任务是什么。2.2 数据格式与预处理流水线BART的输入输出都是文本序列所以数据格式相对简单。我通常把数据整理成JSON Lines格式每行一个样本包含source和target两个字段。预处理流水线包括文本归一化统一全角半角、去除多余空白、长度过滤去掉过短或过长的样本、以及分词器编码。这里有个容易踩的坑BART的输入长度限制。中文BART的max_position_embeddings通常是1024意味着输入和输出加起来不能超过这个长度。但实际做摘要时原文往往超过1024个token。我的处理方式是对原文做截断但截断策略很关键。直接截断尾部会丢失重要信息我一般会保留开头和结尾中间按比例截断或者先用抽取式方法选出关键句再拼接成不超过限制的输入。另一个坑是padding。BART的tokenizer在padding时默认用pad_token_id但中文BART的pad token可能和英文不同。我建议在编码时显式指定paddingmax_length和truncationTrue并且统一设置max_length。如果batch内样本长度差异大用动态padding效率更高但要注意attention mask的正确性。2.3 构建验证集和测试集的注意事项验证集和测试集的构建不能随便切分。中文摘要数据里经常有同一事件的多篇报道如果随机切分训练集和验证集可能包含同一事件的相似文本导致验证指标虚高。我一般会按事件ID或时间戳做分组切分确保同一事件的样本只出现在一个集合中。另外验证集的大小要合理。太小的话指标波动大太大的话训练时评估开销高。我的经验是如果训练集有10万条以上验证集取5000到10000条就够了如果训练集只有几万条验证集至少要有3000条。测试集则要单独留出最好和验证集来自不同时间段或不同来源这样才能真实反映模型的泛化能力。3. 模型加载与微调那些文档不会告诉你的细节3.1 中文BART模型的选型与加载中文BART有几个常见的预训练版本比如在中文维基、新闻、百科等语料上训练的模型。选型时主要看预训练语料和你的业务领域是否匹配。如果你的摘要任务偏向新闻就选新闻语料占比高的版本如果偏向学术或技术文档就选百科或论文语料多的版本。加载模型时我习惯用AutoModelForSeq2SeqLM和AutoTokenizer这样切换模型时只需要改模型名称。但要注意有些中文BART模型的分词器需要额外配置比如tokenizer.src_lang和tokenizer.tgt_lang如果设置不对编码结果会完全错误。我一般会先加载分词器用几个测试句子编码再解码确认分词器工作正常后再加载模型。还有一个细节BART的config里有很多参数比如encoder_layers、decoder_layers、d_model等。如果你要做领域适配可以考虑在预训练模型基础上继续做领域预训练但这需要大量领域语料和计算资源。对于大多数场景直接微调就够了。3.2 微调时的超参数设置经验微调BART做摘要超参数设置直接影响最终效果。我整理了一份自己常用的参数范围供参考参数常用范围说明学习率1e-5 ~ 5e-5太大容易震荡太小收敛慢batch size8 ~ 32受显存限制可用梯度累积训练轮数3 ~ 10看数据量数据少多跑几轮最大输入长度512 ~ 1024根据原文长度分布定最大输出长度64 ~ 256根据摘要长度分布定beam search4 ~ 8生成时用训练时不用length penalty0.6 ~ 1.0控制生成摘要长度学习率是最关键的。我试过用1e-4结果loss剧烈震荡模型完全学不到东西用1e-6又太慢跑十几个epoch都没收敛。后来固定在3e-5左右配合线性预热和衰减效果比较稳。batch size方面如果显存不够可以用梯度累积比如实际batch size是8累积4步等效于32。但要注意梯度累积时学习率要相应调整一般按等效batch size缩放。训练轮数不是越多越好。中文摘要数据通常几万到几十万条我一般跑3到5个epoch然后看验证集ROUGE是否还在提升。如果连续两个epoch验证指标不升反降就说明过拟合了应该早停。3.3 损失函数与标签平滑BART微调默认用交叉熵损失但中文摘要任务里标签平滑label smoothing往往能带来稳定提升。标签平滑把硬标签0和1变成软标签比如0.1和0.9防止模型对某个token过度自信。我一般设置label_smoothing0.1在ROUGE上通常能涨0.5到1个点。另一个技巧是忽略padding token的损失。BART的forward方法默认会计算所有位置的损失包括padding部分。如果padding很多损失会被稀释。我一般会在计算loss时传入attention_mask或者手动把padding位置的label设为-100让损失函数忽略它们。还有一个容易被忽略的点中文摘要的token分布很不均衡。常见字的、了、是出现频率极高生僻字和专有名词出现频率低。如果不做处理模型会倾向于生成常见字导致摘要缺乏信息量。我试过用focal loss或者对低频token加权但效果不稳定。后来发现更简单的方法是调整解码策略比如用top-k采样或核采样增加生成的多样性。4. 解码策略决定摘要质量的关键一步4.1 贪心搜索、束搜索与采样解码策略直接决定生成的摘要长什么样。贪心搜索每步选概率最大的token速度快但容易生成重复、单调的文本。束搜索保留多个候选最终选整体概率最高的序列生成质量通常更好但计算开销大。采样类方法top-k、top-p引入随机性生成更多样但可能不连贯。我在摘要任务上的经验是如果追求稳定和忠实用束搜索beam size设4到6如果追求多样性和可读性用核采样top-p设0.9左右。但要注意束搜索容易生成“安全但平庸”的摘要比如把原文开头抄一遍。核采样则可能生成事实错误的摘要。所以我一般会先用束搜索生成一版再用采样生成一版人工对比后决定用哪个。4.2 长度控制与重复惩罚BART生成摘要时经常出现两个极端要么太短只生成几个词要么太长把原文复述一遍。控制长度的方法有几种一是设置min_length和max_length强制生成在指定范围内二是用length_penalty大于1鼓励长摘要小于1鼓励短摘要三是用no_repeat_ngram_size禁止重复的n-gram。我一般会组合使用min_length20max_length128length_penalty0.8no_repeat_ngram_size3。这样既能保证摘要不会太短又能抑制重复。但要注意no_repeat_ngram_size设太大比如5会导致生成不流畅因为中文里很多合理的重复是必要的。我试过设5结果模型连“的”都不敢重复用句子读起来很别扭。4.3 针对中文的decoding调优中文解码还有一些特殊问题。比如中文BART的分词器可能把“人工智能”切成“人工”和“智能”两个token解码时如果两个token的概率都高模型可能生成“人工智能”这种错误组合。解决方法是在解码时加入词典约束或者用后处理把常见错误组合纠正过来。另一个问题是标点。中文摘要的标点使用和英文不同句号、逗号、顿号、分号各有用途。BART生成的摘要经常标点混乱比如该用句号的地方用了逗号或者连续多个逗号。我一般会在后处理阶段做标点规范化比如把连续逗号替换成句号或者在句末补上句号。还有一个技巧用repetition_penalty控制重复。这个参数大于1时惩罚重复token小于1时鼓励重复。中文摘要里适当惩罚重复比如设1.2能减少“的的的”这类问题但设太高会导致句子不完整。我一般从1.1开始试根据生成结果微调。5. 评估与迭代怎么判断摘要好不好5.1 自动评估指标的局限ROUGE是摘要任务最常用的自动评估指标它计算生成摘要和参考摘要之间的n-gram重叠。ROUGE-1看unigram重叠ROUGE-2看bigramROUGE-L看最长公共子序列。这些指标计算简单但和人类判断的相关性有限。我遇到过ROUGE很高但读起来很差的摘要也遇到过ROUGE一般但信息完整的摘要。BLEU也常被用来评估摘要但它原本是为机器翻译设计的对摘要的评估效果不如ROUGE。BERTScore用预训练模型计算语义相似度比ROUGE更接近人类判断但计算开销大。我一般会同时看ROUGE和BERTScore如果两者趋势一致说明模型确实在进步如果分歧大就需要人工检查。5.2 人工评估的设计与执行自动指标只能做参考最终还是要人工看。我一般会设计几个维度信息完整性摘要是否覆盖原文关键信息、事实准确性摘要是否有原文没有的信息、流畅度摘要是否通顺、简洁度摘要是否冗余。每个维度打分1到5分然后取平均。人工评估的样本量不用太大100到200条就能看出问题。但要注意评估者要多样化不能全是同一个人。我试过让不同背景的人评估同一批摘要结果发现技术背景的人更关注事实准确性非技术背景的人更关注流畅度。所以如果你的摘要面向普通用户就要多找非技术背景的人评估。5.3 错误分析与迭代方向错误分析是迭代的关键。我一般会把生成摘要和参考摘要并排看标记出错误类型漏信息、多信息、指代错误、重复、不连贯等。然后统计各类错误的比例优先解决占比高的。常见的错误和对应策略错误类型可能原因解决方向漏关键信息输入截断、模型容量不足调整截断策略、增大模型生成原文没有的信息过拟合、解码随机性大降低学习率、用束搜索重复生成解码策略问题加重复惩罚、调no_repeat_ngram指代不明中文特性、训练数据不足补充指代标注数据、后处理摘要过长长度控制不当调length_penalty、设max_length我自己的迭代顺序一般是先解决重复和长度问题再解决信息完整性问题最后优化流畅度。因为重复和长度问题最影响可读性信息完整性次之流畅度是锦上添花。6. 部署与性能优化从实验到上线6.1 模型导出与推理加速训练好的BART模型如果要上线需要考虑推理速度。PyTorch模型默认是动态图推理时可以用torch.jit.trace或torch.jit.script导出为TorchScript减少Python开销。也可以用ONNX导出再用ONNX Runtime推理速度通常比原生PyTorch快不少。我试过用ONNX Runtime部署BART在CPU上推理速度提升了约2倍GPU上提升不明显。但ONNX导出时要注意BART的beam search在ONNX里支持有限可能需要把beam search逻辑移到后处理。另一个方案是用TensorRT但配置复杂适合对延迟要求极高的场景。如果不想折腾导出也可以用model.eval()和torch.no_grad()再配合半精度fp16推理速度也能提升。但fp16在CPU上支持不好GPU上要注意数值稳定性。6.2 批处理与动态padding线上推理通常是批处理请求。如果batch内样本长度差异大padding到最大长度会浪费计算。动态padding按batch内最大长度padding能显著提升效率。我一般会在推理前按长度排序然后分batch每个batch内padding到该batch的最大长度。但要注意动态padding时attention mask要正确设置否则padding位置会影响结果。另外如果batch内长度差异太大比如有的样本10个token有的1000个token动态padding的效果也会打折扣。这时候可以分桶把长度相近的样本放在一起。6.3 缓存与并发处理摘要服务通常会有重复请求比如同一篇新闻被多个用户请求摘要。加一层缓存能大幅减少计算。我一般用Redis做缓存key是原文的哈希value是生成的摘要。缓存过期时间根据业务定新闻类可以设几小时文档类可以设几天。并发处理方面如果请求量大可以用消息队列做异步处理避免请求堆积。也可以用多进程或多线程但要注意PyTorch的GIL限制。我一般用FastAPI起服务配合uvicorn的多worker模式每个worker独立加载模型这样能充分利用多核CPU。7. 实际项目中积累的避坑经验7.1 数据层面的坑第一个坑是数据泄露。我早期做实验时训练集和验证集随机切分结果验证ROUGE高得离谱上线后效果一塌糊涂。后来发现是同一事件的报道被分到了不同集合模型其实在“背答案”。解决方法是按事件ID分组切分。第二个坑是摘要长度分布不一致。训练数据里摘要平均20个字但业务需求是50个字的摘要。模型会倾向于生成20字左右的摘要导致信息量不足。解决方法是在训练时加入长度控制或者在解码时强制min_length。第三个坑是特殊符号。中文文本里经常有全角空格、零宽字符、HTML实体等这些在分词时可能被忽略或错误处理。我一般会在预处理阶段用正则清洗把所有非中文、非英文、非数字、非常用标点的字符替换掉。7.2 训练层面的坑第一个坑是学习率设太大。我试过1e-4loss直接飙到nan。后来查资料发现BART微调的学习率一般不超过5e-5。现在我用3e-5配合warmup稳定很多。第二个坑是batch size太小。batch size太小会导致梯度噪声大训练不稳定。如果显存不够可以用梯度累积但要注意调整学习率。我一般用等效batch size 32如果显存只够8就累积4步。第三个坑是忽略验证集。有些人训练时只看训练loss不看验证指标结果过拟合了都不知道。我一般每个epoch结束都跑验证集记录ROUGE连续两个epoch不提升就早停。7.3 推理层面的坑第一个坑是max_length设太小。如果max_length设64但参考摘要平均80个字模型生成到64就被截断摘要不完整。我一般会统计参考摘要的长度分布取95分位数作为max_length。第二个坑是beam search的beam size太大。beam size从4增加到8ROUGE可能只涨0.2但推理时间翻倍。我一般用4或5平衡质量和速度。第三个坑是忽略后处理。BART生成的摘要经常有多余空格、重复标点、不完整句子。我一般会加一层后处理去除多余空格、合并重复标点、补全句末标点、截断不完整句子。8. 一些可以直接用的代码片段8.1 数据预处理与Dataset构建import json import re from torch.utils.data import Dataset class SummaryDataset(Dataset): def __init__(self, data_path, tokenizer, max_input_len512, max_target_len128): self.data [] with open(data_path, r, encodingutf-8) as f: for line in f: item json.loads(line) self.data.append(item) self.tokenizer tokenizer self.max_input_len max_input_len self.max_target_len max_target_len def __len__(self): return len(self.data) def __getitem__(self, idx): item self.data[idx] source self.clean_text(item[source]) target self.clean_text(item[target]) inputs self.tokenizer( source, max_lengthself.max_input_len, truncationTrue, paddingmax_length, return_tensorspt ) targets self.tokenizer( target, max_lengthself.max_target_len, truncationTrue, paddingmax_length, return_tensorspt ) labels targets[input_ids].squeeze() labels[labels self.tokenizer.pad_token_id] -100 return { input_ids: inputs[input_ids].squeeze(), attention_mask: inputs[attention_mask].squeeze(), labels: labels } def clean_text(self, text): text re.sub(r\s, , text) text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、《》\s], , text) return text.strip()这段代码里clean_text做了基本的文本清洗去掉了非中文、非英文、非数字和非常用标点的字符。labels里把padding位置设为-100让损失函数忽略它们。这些细节看起来小但对训练效果影响很大。8.2 微调训练循环from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, AdamW from torch.utils.data import DataLoader import torch model_name your-chinese-bart-model tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSeq2SeqLM.from_pretrained(model_name) train_dataset SummaryDataset(train.jsonl, tokenizer) val_dataset SummaryDataset(val.jsonl, tokenizer) train_loader DataLoader(train_dataset, batch_size8, shuffleTrue) val_loader DataLoader(val_dataset, batch_size8) optimizer AdamW(model.parameters(), lr3e-5) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(5): model.train() total_loss 0 for batch in train_loader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) outputs model( input_idsinput_ids, attention_maskattention_mask, labelslabels ) loss outputs.loss loss.backward() optimizer.step() optimizer.zero_grad() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader)}) # 验证集评估略训练循环本身不复杂关键是超参数。我一般用AdamW学习率3e-5配合线性预热。如果显存不够可以加torch.cuda.amp做混合精度训练。8.3 推理与后处理def generate_summary(text, model, tokenizer, device): model.eval() inputs tokenizer( text, max_length512, truncationTrue, return_tensorspt ).to(device) with torch.no_grad(): outputs model.generate( input_idsinputs[input_ids], attention_maskinputs[attention_mask], max_length128, min_length20, num_beams4, length_penalty0.8, no_repeat_ngram_size3, repetition_penalty1.2, early_stoppingTrue ) summary tokenizer.decode(outputs[0], skip_special_tokensTrue) summary post_process(summary) return summary def post_process(text): text re.sub(r\s, , text) text re.sub(r([。、])\1, r\1, text) if text and text[-1] not in 。: text 。 return text推理时的参数组合是我反复试出来的。num_beams4平衡质量和速度length_penalty0.8鼓励稍短的摘要no_repeat_ngram_size3抑制重复repetition_penalty1.2进一步减少重复token。后处理做了空格去除、重复标点合并、句末标点补全。9. 不同场景下的策略调整9.1 新闻摘要 vs 技术文档摘要新闻摘要通常要求时效性和信息密度摘要要短、要快。我一般用beam searchmax_length设64到96length_penalty设0.6到0.8让摘要更紧凑。技术文档摘要则要求准确性和完整性摘要可以长一些max_length设128到256length_penalty设1.0左右让模型有更多空间组织语言。新闻摘要的另一个特点是实体多。人名、地名、机构名、产品名频繁出现模型容易生成错误实体。我一般会在后处理阶段做实体校验把生成摘要里的实体和原文比对如果原文没有就删掉或替换。9.2 短文本摘要 vs 长文本摘要短文本摘要比如微博、评论输入只有几十到几百字BART可以直接处理不需要截断。但短文本的信息密度低模型容易生成和原文差不多的摘要。我一般会设min_length稍大一些强制模型压缩。长文本摘要比如论文、报告输入可能几千字超过BART的长度限制。我的处理方式是先用抽取式方法选出关键句拼接成不超过1024个token的输入再用BART生成摘要。抽取式方法可以用TextRank或BERTScore选出和原文主题最相关的句子。9.3 多文档摘要的挑战多文档摘要是把多篇相关文档压缩成一篇摘要难度比单文档高很多。BART本身是为单文档设计的直接处理多文档需要把多篇文档拼接起来但这样会丢失文档间的结构信息。我试过用层次化模型先对每篇文档生成单文档摘要再把摘要拼接起来生成最终摘要。效果比直接拼接好但计算开销大。另一个思路是用图模型建模文档间关系但实现复杂。对于大多数场景如果多文档之间主题一致直接拼接加BART也能用如果主题差异大就需要先做聚类或筛选。10. 我个人的一些零散心得做中文摘要这段时间最大的体会是模型不是万能的数据和策略往往比模型本身更重要。我见过太多人花大量时间调模型结构却忽略了数据清洗和评估设计。实际上把数据洗干净、把评估做扎实比换一个更大的模型带来的提升更明显。另一个体会是中文摘要没有“一招鲜”的方案。新闻、论文、对话、商品评论每种文本的摘要需求都不一样解码策略、长度控制、后处理都要相应调整。我一般会针对每个场景单独调一套参数而不是用一个通用配置打天下。还有一点人工评估不能省。自动指标只能做参考最终还是要人来看。我一般会定期抽一批生成摘要自己读一遍标记问题然后针对性优化。这个过程很枯燥但效果最实在。最后如果你刚开始做中文摘要我的建议是先用小规模数据跑通全流程确认数据格式、模型加载、训练循环、推理生成都没问题再逐步扩大数据量和模型规模。不要一上来就上百万数据、最大模型那样出了问题很难定位。小步快跑快速迭代才是做摘要任务最稳妥的方式。