简介面向自然语言处理开发者这份压缩包是基于BERT微调实现抽取式摘要的完整工程代码围绕BertSum-master项目展开适合需要复现摘要论文或深入理解预训练模型微调流程的读者。资源共36个文件以20个Python脚本为核心涵盖数据预处理、模型构建、分布式训练与评估模块另有7个TXT映射文件、2个JSON配置、1个Markdown说明及License协议压缩包整体约14.99MB。目前已有1523人学习下载。代码覆盖了从原始文本分词、添加[CLS]与[SEP]特殊标记、生成Token及Mask输入到加载预训练BERT权重、在顶部构建序列到序列解码器、采用交叉熵损失与Adam优化器训练直至ROUGE指标评估与摘要后处理的完整步骤同时附带cnndm样本数据集和多个文本映射文件可快速跑通实验流程。通过阅读与调试源码能够清晰掌握抽取式摘要的完整技术链路并加深对BERT双向语义表示、Transformer自注意力机制以及序列到序列建模实践应用的理解。1. 微调BERT做抽取式摘要这个标题的代码到底在做什么Python 微调 BERT 用于提取摘要的论文代码说白了就是把 BERT 改造成一个句子打分器从原文里按重要性挑句子拼成摘要。做文本摘要的人可能都有过这种体验生成式模型输出的摘要很流畅但偶尔会把原文没有的事实填进去抽取式摘要不生成新句子只挑原句可控性好很多。标题里提到的提取摘要就是指抽取式摘要Extractive Summarization这类代码解决的是新闻聚合、报告压缩、会议纪要等需要忠于原文的场景。对刚接触 NLP 微调的人来说它也是一个很适合练手的二分类任务数据好构造、效果可量化、复现路径清晰。下面我从数据、训练、评估、踩坑和改造五个层面把我自己的复现思路讲清楚。2. 复现微调BERT摘要代码前的数据与模型准备2.1 抽取式摘要的标准范式BERT怎么给句子打分抽取式摘要最常见的实现是把摘要问题转换成句子级二分类问题。流程是先把文档按句号、问号等切分成句子每个句子用 BERT 编码取 [CLS] 位置的向量接一个线性分类层输出是否入选摘要的概率。用论文代码里的说法就是给每个句子一个 0/1 标签微调时让模型最小化交叉熵损失。与生成式摘要不同这个过程不产生新词所以摘要里的每个词都能溯源到原文特别适合对事实性要求很高的业务。这里有个关键认识BERT 本身是语言模型不会直接输出哪句重要。我们微调它本质是让模型学会从上下文推断句子重要性。所以输入不能只喂一个孤立的句子一般会把整篇文档或一个窗口内的上下文和当前句子拼在一起或者把每句单独编码。多数论文代码采用每句作为一个样本但要带上文档其他句子的信息。实操里最简单的是对每个句子独立编码但效果会差不少因为没有上下文时他这这类指代词完全无法消解。我一般会采用句子对方式把当前句子和文档前几句/全文拼接中间用 [SEP] 分隔然后让 BERT 对整个序列编码取 [CLS] 做分类。这样做能让模型参考上下文。不过还要考虑 BERT 的输入长度上限常见是 512 个 token所以长文档需要滑窗或者先选择候选句。如果目标文档是新闻通常可以截取前 512 token如果是论文或报告就必须做更细致的片段处理。常见做法是直接参考某篇论文的源码里面会有一个SentenceDataset类负责切句、编码和标签对齐。我们可以照着这个思路自己实现不依赖原作者代码也能复现。你要做的第一件事不是急着下载模型而是先把数据格式定义清楚一个样本包含doc_id、sentence、position、label四个字段其中position是为了推理时还原顺序。2.2 数据格式从原始文本到标签序列的整理脚本复现的第一步是把原始数据整理成模型能吃的格式。以新闻文档为例输入是一篇篇文档每篇配一个参考摘要一般是人工写的摘要抽取式训练标签往往用贪心算法从参考摘要里选句子生成或者直接人工标注。这里需要说明一下标签怎么来如果用现成摘要数据集通常参考摘要是抽象式人工重新组织的没办法直接当句子标签。常见做法是用句子与参考摘要的ROUGE相似度做匹配给原文每个句子算一个 ROUGE 分数超阈值标为 1。这也是论文代码里最常见的数据处理步骤。ROUGE 分数计算要先把原文句子和参考摘要都做 n-gram 切分然后看有多少 n-gram 命中了参考摘要。通常用rouge1和rouge2的 F1 值混合起来做阈值判断比单看recall稳定。下面是一个简化版的切句与标签生成脚本import re from dataclasses import dataclass dataclass class SentenceSample: doc_id: str sentence: str position: int label: int def split_sentences(text: str): # 简单按中英文标点切句生产环境建议用sentence splitter parts re.split(r([。!?]), text) sentences [] buf for p in parts: buf p if p in 。!?: sentences.append(buf.strip()) buf if buf.strip(): sentences.append(buf.strip()) return sentences def build_samples(doc_text: str, doc_id: str, rouge_scores: list, threshold: float 0.25): sentences split_sentences(doc_text) samples [] for i, sent in enumerate(sentences): label 1 if rouge_scores[i] threshold else 0 samples.append(SentenceSample( doc_iddoc_id, sentencesent, positioni, labellabel )) return samples逻辑说明split_sentences先按中英文标点切句再把标点拼回前一个句子build_samples拿预先算好的 ROUGE 分数列表和阈值做二值化。position字段很重要后面训练时可以用它拼位置特征也可以用它在推理时还原原文顺序防止句子乱序。这个脚本没有做引号、括号内句号的边界判断遇到复杂文本时标签会错位但作为复现起点够用了。参数说明threshold越高标签越稀疏摘要句子越少训练时正样本越少一般先用 0.20.3 起步然后观察验证集 ROUGE。这里没有用复杂的分句库是因为很多论文代码为了可复现性用的就是正则切句。真要复现时先跑通这个版本再考虑换spaCy等工具。另外要注意如果标签全是 0模型会学成一个句句不重要的退化分类器所以先统计一下数据里正样本比例太低的话要调低threshold或者换rougeL指标。有一点经常被忽略切句后要保留句子在原文中的字符偏移量。这个偏移量在构造 BERT 输入时可以用于从原文截上下文避免每次用find(sent)找句子位置既能提升速度也能避免短句重复匹配的问题。保存偏移量其实就是position之外的另一个字段char_start在build_samples里顺手加进去就行。2.3 模型加载与分词为什么不能直接把长文档塞给BERTBERT 的位置编码是训练时固定到 512 位的超过 512 个 token 的序列会被截断。抽取式摘要的文档经常超过这个长度如果直接截断后面的句子得不到任何表示标签也就无法学习。更麻烦的是即使文档只有 300 token如果同时把整篇文档和当前句都放进去也很容易超长因为 tokenizer 会把英文单词拆成子词长句轻松占掉几百 token。常见的处理方案有三种第一只取前 512 token代价是忽略文档后半段第二把文档分成多个片段每个片段内预测句子第三用滑窗让每个句子都能以自己为中心获得上下文。论文代码里比较多的是第二种先把文档切句再与足够长的上下文拼接超长就从当前句前后截取。第三种滑窗需要处理重叠和句子跨窗口的问题工程复杂度更高先不建议新手碰。下面演示分词和样本构造使用transformers库from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-uncased) def encode_sample(sample, doc_text, max_len512): # 把整篇文档作为上下文拼接超长时从当前句附近截取 sent sample.sentence context_start doc_text.find(sent) if context_start -1: context doc_text else: # 当前句前后各留一部分上下文 start max(0, context_start - 150) end min(len(doc_text), context_start len(sent) 300) context doc_text[start:end] encoding tokenizer( context, sent, truncationonly_first, # 只截断上下文保留当前句 paddingmax_length, max_lengthmax_len, return_tensorspt ) return { input_ids: encoding[input_ids][0], attention_mask: encoding[attention_mask][0], token_type_ids: encoding[token_type_ids][0], label: sample.label, position: sample.position }逻辑说明tokenizer(context, sent)会生成[CLS] context [SEP] sent [SEP]的结构truncationonly_first表示上下文超长时优先截断 context 而不动当前句。这个细节很重要否则当前句可能被截没了标签就学不到有效信息。在训练时不需要每次都doc_text.find(sent)因为可能有同名短句正确做法是在构建样本时就记录char_start然后通过char_start计算上下文范围。参数说明max_len建议先用 256 跑通再看显存往上加context_start用字符串查找可能有误差更稳妥的办法是在切句时就带上全局索引。只要确保当前句子完整出现在 BERT 输入序列里模型才有机会学会这句重要不重要。token_type_ids用于区分前/后句微调时 BERT 会学习上下文 vs 当前句的差异这也是句子级分类任务和普通文本分类的差别。很多代码复现失败就是漏了token_type_ids导致两个序列混合模型效果大降。此外加载模型时最好明确num_labels和ignore_mismatched_sizes。如果你从一个已经微调过分类头的 checkpoint 继续训练分类头维度可能不匹配from_pretrained(..., num_labels2, ignore_mismatched_sizesTrue)可以把旧分类头扔掉但这只是权宜之计最稳妥的还是用预训练 checkpoint 开始微调。3. 动手微调BERT训练脚本骨架与关键参数3.1 用Trainer还是自己写训练循环Hugging Face 的Trainer封装了很多细节适合快速验证但如果想完全按论文代码走比如自定义采样策略、控制正负样本比例自己写训练循环更直观。我一般先在标准数据集上跑通 Trainer再改成自定义循环复现论文里的设置。Trainer 的优势是能自动处理设备分配、梯度累积和断点续训这些在复现时很省时间。先看 Trainer 的最小用法from transformers import BertForSequenceClassification, Trainer, TrainingArguments from datasets import Dataset model BertForSequenceClassification.from_pretrained( bert-base-uncased, num_labels2, problem_typesingle_label_classification ) training_args TrainingArguments( output_dir./bert_summary_checkpoints, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs3, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, greater_is_betterTrue, metric_for_best_modeleval_f1, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_ds, eval_dataseteval_ds, compute_metricscompute_metrics, ) trainer.train()逻辑说明BertForSequenceClassification会把 [CLS] 向量接一个线性分类头这正是抽取式摘要需要的结构。problem_typesingle_label_classification会默认用交叉熵损失。Trainer内部默认使用AdamW并且会自动做梯度裁剪这对 BERT 微调很重要。参数说明微调 BERT 的learning_rate一般取2e-5到5e-5太大容易灾难性遗忘太小收敛慢。per_device_train_batch_size受显存限制BERT base 在 512 长度下 16 是常规起步值。evaluation_strategyepoch表示每个 epoch 做一次验证比较省时如果数据量大可以改成steps并配合eval_steps。load_best_model_at_end确保训练结束保存的是验证集最优的模型而不是最后一个 epoch 的模型——这是摘要任务里很常见的后悔药可以多存两个 checkpoint 防止最好的模型被覆盖。除了learning_rate还需要设置weight_decay。BERT 微调常常配合 0.01 的 weight decay能轻微抑制过拟合。如果训练数据不大建议在TrainingArguments里加上weight_decay0.01。另外warmup_ratio也很重要一般设成 0.1也就是在前 10% 的 step 里把学习率从 0 线性升到目标值。没有 warmupBERT 在训练初期会出现 loss 冲高然后突然下降的现象虽然最终也能收敛但对复现论文的实验不太友好。3.2 标签对齐细节句子向量和原始位置的映射训练样本构造完模型只能输出label概率但推理时需要把预测的句子位置还原到原文档。很多论文代码里数据集会额外保存一个orig_id或句子在文档里的序号而不是靠列表顺序。下面是训练循环里必须注意的对齐逻辑# 预测后的后处理把文档内部的句子重新排序 pred_dict {} # doc_id - list of (position, predict_prob) for batch in eval_dataloader: outputs model(**batch) probs torch.softmax(outputs.logits, dim-1)[:, 1] for i, doc_id in enumerate(batch[doc_id]): pred_dict.setdefault(doc_id, []).append( (batch[position][i].item(), probs[i].item()) ) # 按position排序再按概率倒序选Top K def select_summary_sentences(doc_id, k3): items sorted(pred_dict[doc_id], keylambda x: x[0]) items_sorted_by_prob sorted(items, keylambda x: x[1], reverseTrue) selected_positions sorted([p for p, _ in items_sorted_by_prob[:k]]) return selected_positions逻辑说明position是句子在文档里的序号后处理时先按位置排序再按概率取 Top-K最后把选中的位置重新排序输出。如果不做这一步模型在训练时是独立预测每句话推理时句子顺序会乱拼出来的摘要可读性差。要注意pred_dict必须以doc_id为键否则不同文档的句子会被混在一起算排序。如果数据集非常大一次性把pred_dict放内存可能不够。常见做法是先按doc_id分组处理完一个文档就立即拼接摘要释放内存。另外一个技巧是推理时用torch.cuda.amp.autocast()开混合精度能省一半显存但要注意模型推理输出的 logits 会被自动转回 float32不需要额外处理。在训练数据里position不能参与 loss 计算它只作为元信息。有的论文代码会把position也作为一维特征输入模型常见做法是拼在 [CLS] 向量后面做一个简单特征增强或者干脆用分段位置向量。我不太推荐在初版就这么做因为增加了特征列和对齐复杂度先跑通基线再优化位置信息会更容易定位问题。3.3 验证指标与早停别只看accuracy抽取式摘要的正负样本通常严重不平衡一篇文档 20 句话可能只有 3 句是摘要句。如果监控accuracy模型只要全预测为 0 就能拿到很高分数但生成的摘要没有意义。所以验证指标必须用 F1或者直接用 ROUGE-F1而不是自定义的预测对了几句话。一个能用的compute_metrics示例from sklearn.metrics import f1_score def compute_metrics(eval_pred): logits, labels eval_pred preds logits.argmax(-1) f1_macro f1_score(labels, preds, averagemacro) f1_pos f1_score(labels, preds, pos_label1) return {f1_macro: f1_macro, f1_pos: f1_pos}逻辑说明f1_macro把两个类同等看待能防止模型靠无脑预测 0 刷分f1_pos只看正类更贴近摘要需求。早停配置里metric_for_best_modeleval_f1或具体的eval_f1_pos要比accuracy靠谱得多。如果你用的是 Trainer注意compute_metrics返回的 dict key 会根据eval_前缀自动生成监控项是eval_f1_macro。参数说明如果你的显卡允许更大 batch可以把eval_batch_size调大因为推理不需要保存梯度32 或 64 都比较安全。验证时不建议做样本增强也不要做随机采样必须全量评估否则 F1 波动大早停容易误判。还有一个小习惯每个 epoch 存一次模型但只保留 F1 最高的那个这样训练出错时还能回滚到上一版不用从头再来。超参数推荐值说明learning_rate2e-5BERT base 常用起步值收敛稳定weight_decay0.01轻微正则避免微调过拟合warmup_ratio0.1训练初期平滑学习率max_len256512由显存和目标文档长度决定batch_size16等效不足时用梯度累积补足epochs35小数据集 3 个够了看验证 F1 早停4. 复现微调BERT摘要代码的五个高频坑与排查记录4.1 长文本被截断摘要句子全没了现象训练 loss 正常下降但验证时生成的摘要全是原文前半段无意义句子模型似乎没学到后半段。原因数据加载时直接对整篇文档做truncationTrue所有超过 512 的句子都被丢到序列末尾而 BERT 的位置编码在长文本上表达能力有限导致后半段句子几乎没有梯度信号。你的输入里可能根本没有那些句子自然学不到它们。解决改用前面 2.3 节的方式先把文档切句再让每个句子带上下文编码。必须确保每个样本都有context和sentence两个 segment且truncationonly_first。改成这个结构后后半段句子也能被模型看到。验证方法是打印训练样本里的input_ids反解出句子文本确认当前句是否完整出现在尾部。4.2 分词后的标签错位现象训练集里明明只给句子打了标签但训练时 label 总对不上或者 loss 一直异常高。原因最典型的是直接把整篇文档 tokenize然后对每个 token 打标签。但分词器会把一个句子拆成多个 token导致 token 级别 labels 和句子级别 labels 长度不一致。另一个可能是在token_type_ids上做预测却拿句子级堆叠的标签去对维度对不齐。解决抽取式摘要按句子粒度建模不要按 token 建模。正确流程是先把文档切成句子 → 句子级编码 → 每个句子样本一个 label。如果硬要用 token 级标签也要按 token 归属的句号对齐但没必要。这个错位是复现论文代码时最容易踩的坑十有八九是数据加载器写错了。排查时打印一个 batch 的input_ids.shape和labels.shape第一维度应该都是 batch size而不是序列长度。提示与其在训练时报错后猜不如在构造数据集时就写一个自检函数遍历 20 个样本断言input_ids[0]对应的句子文本与sample.sentence一致。这能省下大量排查时间。4.3 类别不均衡模型全预测0现象验证集 F1_pos 一直是 0模型把所有句子都预测为负样本。原因一个文档 30 句标签只有 23 句是 1正样本占比不到 10%。BERT 的 dropout 和分类头初始化都偏向类别均衡但极端不均衡时模型会学偏尤其是用accuracy做早停时退化解会被选中。解决先从样本权重入手给正样本加 loss 权重。用BertForSequenceClassification时可以设置model.classifier的输出后手动算weighted_loss nn.CrossEntropyLoss(weighttorch.tensor([1.0, 10.0]))。更通用的做法是训练时对正样本过采样把每个文档的正样本复制若干份。调权重时别一上来就 50:1先试 5:1观察 F1_pos 变化。如果还不行可以换focal loss但先别引入过多复杂度。4.4 显存不足batch size调小后效果崩现象per_device_train_batch_size16时会 OOM改成 4 后训练 loss 不降甚至不收敛。原因batch size 太小会导致梯度估计抖动BERT 微调本来就需要相对较大的 batch。另外如果只改 batch size 不改学习率也会放大抖动。batch size 和显存之间多少有点玄学很多人一碰到 OOM 就无脑把 batch down 成 2结果效果全面崩溃。解决如果显存不足先降max_len比如 512→384而不是一味降 batch size或者用gradient_accumulation_steps模拟大 batch。例如想保持等效 batch size 16实际 batch size 4 时设gradient_accumulation_steps4。学习率可以尝试按 batch size 比例微调但不要直接开到 5e-5。建议在训练日志里记录 loss观察剧烈波动就说明有效 batch 太小。4.5 ROUGE分数上不去但loss在降现象训练和验证的 loss 都在降F1 也还行但最后用生成的摘要算 ROUGE分数远低于数据集参考值。原因评价指标错位。F1 是句子级指标ROUGE 是词级 n-gram 指标。模型在句子级分类上优秀不代表按 Top-K 拼出的摘要能对齐参考摘要的关键词。另外参考摘要往往是抽象式的句子抽取天然对不上。解决验证阶段就引入近似 ROUGE 计算。不能为了简单只算 F1。可以在每个 epoch 结束后用当前模型预测开发集的标签分数然后按 Top-K 拼摘要、跑一遍 ROUGE看趋势。注意这里用rouge_score库时要先统一文本规范化中文要分词或按字切分否则 ROUGE 会被低估。还有一个容易被忽略的问题参考摘要可能只有一句话你抽取两句去算 ROUGE会稀释 precision。所以评估时要设定摘要长度与参考摘要接近而不是固定抽 3 句。5. 评估与推理从预测概率到拼出像样的摘要5.1 ROUGE评估的两种写法与陷阱ROUGE 有多种实现常用的有rouge_score库和rouge库。复现论文代码时建议直接用论文里指定的指标变体比如 ROUGE-1-F, ROUGE-2-F, ROUGE-L-F。很多论文报告的是 F1 而不是 ROUGE-1 recall因为 F1 更能反映抽取式的平衡性。一个可靠的手写 ROUGE-1 计算示例from rouge_score import rouge_scorer scorer rouge_scorer.RougeScorer([rouge1, rouge2, rougeL], use_stemmerTrue) def compute_rouge(hypothesis, reference): scores scorer.score(reference, hypothesis) return { rouge1_f: scores[rouge1].fmeasure, rouge2_f: scores[rouge2].fmeasure, rougeL_f: scores[rougeL].fmeasure, } hyp 这是生成的摘要句子。 ref 这是参考摘要。 print(compute_rouge(hyp, ref))逻辑说明scorer.score(reference, hypothesis)这个参数顺序容易写反写反会导致 ROUGE 不稳定或偏低。use_stemmerTrue会把英文单词还原中文没有词干问题但要注意空格分词。ROUGE 的计算是参考摘要决定分母所以把 hyp 放前、ref 放后会改变 recall 和 precision最终 F1 也可能不一样。参数说明中文摘要建议先把文本按字符切分或者在score前统一用jieba分词后以空格连接否则 ROUGE 会把每个汉字当成 token指标失真。计算 ROUGE 时参考摘要和生成摘要都要去掉多余换行和空格做到完全规范化。ROUGE-1 看单字/单词重合ROUGE-2 看双词序ROUGE-L 看最长公共子序列摘要任务里报告 ROUGE-1 和 ROUGE-L 居多ROUGE-2 偏低是正常的。用rouge_score时还会遇到一个问题它对空字符串会抛异常。在评估一批摘要时如果某篇模型没抽到句子hyp为空整个评估进程会断。所以要在计算前加判断if not hyp or not ref: continue或者填一个默认占位符。这种小坑在生产环境经常出现最好提前写好。5.2 推理时的句子筛选策略Top-N与阈值结合训练完模型推理阶段需要决定抽多少句。最直接的是固定 Top-KK 根据参考摘要长度估算。但文档长短不一固定 K 会导致短文档抽多了、长文档抽少了。论文代码里常见做法是先按概率降序抽到摘要长度接近目标长度的正负区间再按原位置排序。一个稳妥的推理函数假设你已经基于 2.3 的encode_sample改出了一个只用于推理的encode_sample_for_inference去掉 label 字段即可def extract_summary(model, tokenizer, doc_text, target_len120): samples split_sentences(doc_text) scored [] for i, sent in enumerate(samples): enc encode_sample_for_inference(sent, doc_text, tokenizer) with torch.no_grad(): logits model(**enc).logits prob torch.softmax(logits, dim-1)[0, 1].item() scored.append((prob, i)) scored.sort(keylambda x: x[0], reverseTrue) selected [] total_len 0 for prob, idx in scored: if total_len len(samples[idx]) target_len: continue selected.append(idx) total_len len(samples[idx]) if total_len target_len: break selected.sort() return 。.join([samples[i] for i in selected]) 。逻辑说明先按概率倒序访问句子贪心地往摘要里加句子直到长度接近target_len。这种策略比纯 Top-K 更符合实际场景因为摘要长度是用户可期望的资源而不是固定句子数。选中的句子最后要按原文档位置排序否则摘要可读性很差。贪心的缺点是有可能跳过一两个长句导致最后选不够目标长度所以可以把target_len放宽 15%或者允许最后一句超一点。参数说明target_len可以按原文长度的 30% 估算也可以直接根据参考摘要训练集的平均长度来定。如果该段文本里有导语性质的第一句可以在概率基础上加一个小偏置这个我在最后一章会讲。推理时注意关闭 dropout也就是用model.eval()否则同一个句子的概率每次都会变。还有一个更细的调整设定一个最小概率阈值比如prob 0.5的句子不选防止模型对一个全是低分但长度很短的文档硬凑摘要。如果你的业务需要摘要一定存在可以把min_prob降到 0.3再不行就选第一句兜底。5.3 可视化抽查把预测分数标回原文调试模型时只算指标不够最好把每句的预测分数打印出来对照原文看模型有没有学到首句重要复述核心信息的句子重要这些模式。下面是个简单脚本def inspect_predictions(model, tokenizer, doc_text): samples split_sentences(doc_text) encs [encode_sample_for_inference(s, doc_text, tokenizer) for s in samples] with torch.no_grad(): probs [] for enc in encs: logits model(**enc).logits probs.append(torch.softmax(logits, dim-1)[0, 1].item()) for i, (s, p) in enumerate(zip(samples, probs)): print(f[{i:02d}] p{p:.2f} {s})逻辑说明打印时保留序号和概率方便对照文档中间部分的高分句子。如果发现模型对包含数字、专有名词的句子打分明显高说明模型学到的是表面线索而不是真正的语篇重要性。这种人工抽查比只看 ROUGE 更有价值。注意推理时encode_sample_for_inference不能用训练时的随机截断策略要把整段文档做成稳定的上下文否则同一个句子在不同上下文下分数会抖动。这是推理和训练之间最容易忽略的差异。更严格的做法是在开发集上固定一个context_window每次都以当前句为中心前后各取固定长度的上下文而不是用整篇文档。6. 让抽取摘要更稳的一个落地技巧给句子位置加一个先验偏置到这里复现流程基本通了。最后一个值得做的改造把句子位置信息显式融入打分。很多论文代码里BERT 是独立对每个句子和二段上下文编码的但位置信息只存在于 position embedding 里不够直接。实际业务场景里新闻首句、启承转合句的重要性有明显先验。我常用的做法是把预测概率和一个位置先验相乘。位置先验可以用一个简单函数生成比如给前 5 句一个线性衰减的偏置分数或者干脆用文档内的相对位置pos / total_sentence构造一个正弦曲线。然后把最终排序分数改成alpha * model_prob (1 - alpha) * position_prior其中alpha通常取 0.8。这个技巧在长文档上比纯模型概率更稳因为 BERT 受 512 token 限制长文档中间的句子往往缺乏足够上下文。代码大概是这样的def position_prior(idx, total, modelinear): if total 1: return 1.0 if mode linear: return max(0.0, 1.0 - idx / total) if mode top_k: return 1.0 if idx min(3, total) else 0.6 return 0.5 def final_score(model_prob, idx, total, alpha0.8): return alpha * model_prob (1 - alpha) * position_prior(idx, total)推理时把final_score作为排序依据再进行贪心长度控制。我自己的习惯是先用纯模型概率跑一遍开发集记录哪些句子被模型选但没被参考摘要选中再看这些句子是不是大量出现在文档中后部。如果是就把位置偏置的alpha调低到 0.7。反之如果模型已经能很好利用上下文就调高alpha避免位置先验盖过模型判断。这个改法最大的价值是不需要重新训练模型模型权重还是那个微调好的 BERT只是推理阶段多了一个排序因子。落地时可以直接上线风险很低。记得在验证集上比较一下改动前后的 ROUGE别凭感觉调。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?