简介:这份资源是面向自然语言处理学习者与开发者的BERT抽取式摘要实战代码包,基于BertSum-master项目,帮助读者理解如何将预训练BERT微调应用于文本摘要生成任务。包内共36个文件,以20个Python脚本为主,涵盖数据预处理、模型定义、训练与评估等核心流程,另含7个txt映射文件、2个json配置与样例数据、1个README说明及LICENSE等,压缩包约14.99MB,目录按数据、模型、脚本分模块组织,便于按流程阅读。已有1525人学习下载,适合具备一定深度学习基础、希望动手复现论文摘要模型的读者。通过梳理代码,可掌握Token IDs与Mask构造、编码器-解码器结构搭建、交叉熵损失与Adam优化、ROUGE评估及生成结果后处理等关键环节,是理解BERT在序列到序列任务中落地方式的实践参考。
1. 从一份论文代码说起:BERT 抽取式摘要到底怎么落地
很多人第一次接触抽取式摘要,是在论文里看到一串漂亮的 ROUGE 分数,然后兴冲冲去找开源实现,结果打开仓库发现只有几个.py文件和一堆没写清楚的超参,跑起来不是显存爆掉就是数据格式对不上。这份 Python 微调 BERT 做摘要提取的论文代码,解决的正是这个断层:它把「用预训练 BERT 做句子级二分类、挑出原文中最该保留的句子拼成摘要」这条链路完整写了出来,包含数据预处理、模型封装、训练循环和推理打分。适合两类人:一是想复现论文结果、搞清楚抽取式摘要内部机制的学生和算法初学者;二是手里有一批长文档、想快速搭一个能用的摘要基线再往上叠业务逻辑的开发者。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆。
2. 项目结构与技术选型:为什么是 BERT + 句子分类
2.1 整体架构:把摘要问题降维成逐句打分
抽取式摘要和生成式摘要最大的区别是:它不造新句子,只从原文里挑句子。这个项目采用的就是最经典也最稳的做法——把文档切成句子,每个句子前面拼上[CLS],让 BERT 输出一个二分类概率,表示「这句该不该进摘要」。训练时用人工摘要和原文句子的重叠度构造标签,推理时对所有句子打分、排序、按长度预算截断。
整条链路可以拆成四层:
| 层次 | 职责 | 对应模块 |
|---|---|---|
| 数据层 | 文档分句、构造样本、生成标签 | 数据预处理脚本 |
| 模型层 | BERT 编码 + 分类头 | 模型封装类 |
| 训练层 | 损失计算、反向传播、评估 | 训练主循环 |
| 推理层 | 逐句打分、选句、拼摘要 | 推理脚本 |
这种分层的好处是每一层都能单独替换。比如你后面想换 RoBERTa,只动模型层;想换标签构造策略,只动数据层。常见做法是把配置抽到一个config字典或argparse里,避免超参散落在各个文件。
2.2 目录结构:一个能跑的最小工程长什么样
论文代码往往不追求工程规范,但至少要能让人找到入口。按这类项目的典型组织,目录大致如下:
bert-summarization/ ├── data/ │ ├── raw/ # 原始文档与参考摘要 │ └── processed/ # 分句、编码后的训练样本 ├── src/ │ ├── preprocess.py # 分句、标签构造、tokenize │ ├── dataset.py # Dataset / DataLoader 封装 │ ├── model.py # BERT + 分类头 │ ├── train.py # 训练与验证循环 │ └── predict.py # 推理与摘要拼接 ├── configs/ │ └── base.json # 超参、路径配置 ├── requirements.txt └── README.mddata/raw放原始语料,data/processed放预处理产物,这样重跑预处理不会污染原始数据。configs/base.json集中管理max_seq_length、batch_size、learning_rate这些关键参数。我一般会再加一个outputs/目录存 checkpoint 和日志,避免模型文件和数据混在一起。
2.3 依赖与版本:别在环境上翻车
这类项目对transformers和torch的版本比较敏感,尤其是BertTokenizer的do_lower_case行为和TrainerAPI 在不同版本间有差异。建议锁一个能跑通的组合:
pip install torch==1.13.1 pip install transformers==4.26.1 pip install scikit-learn nltk tqdmnltk用来做英文分句(sent_tokenize),中文场景要换成jieba或按标点切分。scikit-learn主要用来算评估指标。注意transformers4.26 之后部分旧接口有弃用警告,但不影响运行;如果直接装最新版,Trainer的参数名可能对不上,这是最常见的环境坑。
3. 核心模块拆解:从分句到摘要输出的完整链路
3.1 数据预处理:标签怎么造才合理
抽取式摘要的训练标签不是现成的,得从「参考摘要」反推。标准做法是:对原文每个句子,计算它和参考摘要的 ROUGE 或词重叠度,超过阈值标 1,否则标 0。这样模型学的是「哪些句子更像人工会选的句子」。
import re from nltk.tokenize import sent_tokenize def split_sentences(text): # 先按段落切,再段内分句,避免跨段错误合并 paragraphs = [p.strip() for p in text.split("\n") if p.strip()] sentences = [] for para in paragraphs: sentences.extend(sent_tokenize(para)) return sentences def build_labels(sentences, reference_summary, threshold=0.3): ref_tokens = set(re.findall(r"\w+", reference_summary.lower())) labels = [] for sent in sentences: sent_tokens = set(re.findall(r"\w+", sent.lower())) if not sent_tokens: labels.append(0) continue overlap = len(sent_tokens & ref_tokens) / len(sent_tokens) labels.append(1 if overlap >= threshold else 0) return labelssplit_sentences先按段落再分句,是为了防止sent_tokenize把跨段内容错误拼接。build_labels里的threshold是关键参数:设太高会导致正样本太少、模型全预测 0;设太低则正样本泛滥、摘要冗余。经验值在 0.2 到 0.4 之间,具体看参考摘要长度。overlap用句子自身词数做分母,而不是参考摘要词数,这样长句不会被系统性压低分数。
3.2 模型封装:BERT 加一个二分类头
模型部分很直接:BertModel取[CLS]位置的输出,接一个Linear(hidden_size, 2)。关键是forward里要把attention_mask传对,否则 padding 位置会污染[CLS]表示。
import torch import torch.nn as nn from transformers import BertModel class BertSentenceClassifier(nn.Module): def __init__(self, model_name="bert-base-uncased", num_labels=2, dropout=0.1): super().__init__() self.bert = BertModel.from_pretrained(model_name) hidden = self.bert.config.hidden_size self.dropout = nn.Dropout(dropout) self.classifier = nn.Linear(hidden, num_labels) def forward(self, input_ids, attention_mask, token_type_ids=None): outputs = self.bert( input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids, ) cls_output = outputs.last_hidden_state[:, 0, :] # [CLS] 向量 logits = self.classifier(self.dropout(cls_output)) return logitsmodel_name可以换成任意 HuggingFace 上的 BERT 变体,中文场景换成bert-base-chinese。dropout放在分类头前是常规操作,防止过拟合;如果数据量很小,可以调到 0.2 到 0.3。token_type_ids在单句分类里其实用不上,但保留参数是为了兼容某些 tokenizer 的输出,传None时 BERT 内部会自己处理。
3.3 训练循环:损失、评估与早停
训练用标准的交叉熵,但要注意类别不平衡——大多数句子是负样本。常见做法是给正样本加权,或者用pos_weight传给BCEWithLogitsLoss。这里用CrossEntropyLoss配合weight参数。
from torch.optim import AdamW from transformers import get_linear_schedule_with_warmup def train_one_epoch(model, dataloader, optimizer, scheduler, device, pos_weight=2.0): model.train() total_loss = 0.0 # 正样本加权,缓解类别不平衡 weight = torch.tensor([1.0, pos_weight], device=device) criterion = nn.CrossEntropyLoss(weight=weight) for batch in dataloader: input_ids = batch["input_ids"].to(device) attention_mask = batch["attention_mask"].to(device) labels = batch["labels"].to(device) optimizer.zero_grad() logits = model(input_ids, attention_mask) loss = criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() total_loss += loss.item() return total_loss / len(dataloader)pos_weight控制正样本的相对权重,设 2.0 意味着正样本的损失贡献是负样本的两倍,适合正样本占比 20% 到 30% 的情况。clip_grad_norm_的max_norm=1.0是防梯度爆炸的保险,BERT 微调时几乎必加。scheduler用线性 warmup,warmup 比例一般设总步数的 10%,学习率2e-5到5e-5之间。
3.4 推理与摘要拼接:打分之后怎么选句
推理阶段对每个句子单独打分,然后按概率排序,在长度预算内依次选入。这里有个细节:选句时要尽量保持原文顺序,否则摘要读起来跳跃。
def summarize(model, tokenizer, document, max_length=512, budget=3, device="cuda"): sentences = split_sentences(document) model.eval() scored = [] with torch.no_grad(): for idx, sent in enumerate(sentences): enc = tokenizer(sent, truncation=True, max_length=max_length, padding="max_length", return_tensors="pt") logits = model(enc["input_ids"].to(device), enc["attention_mask"].to(device)) prob = torch.softmax(logits, dim=-1)[0, 1].item() scored.append((idx, prob, sent)) # 按概率降序选前 budget 句,再按原序排列 top = sorted(scored, key=lambda x: x[1], reverse=True)[:budget] top_sorted = sorted(top, key=lambda x: x[0]) return " ".join([s for _, _, s in top_sorted])budget是摘要句数上限,按业务需求定,一般取原文句数的 20% 到 30%。max_length=512是单句编码上限,超长句会被截断,所以预处理时最好先把超长句再切一刀。torch.softmax取第 1 维概率作为「该句入选」的置信度。按原序重排这一步很多人会漏,导致摘要逻辑断裂,属于血泪经验。
4. 跑起来:环境、数据与训练命令
4.1 数据准备:格式对齐是第一步
项目期望的原始数据通常是「一篇文档一个文件,参考摘要一个文件」,或者一个 JSON 列表。常见格式如下:
[ { "document": "第一段内容。第二段内容。", "summary": "参考摘要句子。" } ]预处理脚本读入后,对每篇文档分句、构造标签,再用 tokenizer 编码成input_ids、attention_mask、labels,存成.pt或直接喂给Dataset。注意document里不要混入 HTML 标签或多余空白,否则分句结果会很脏。
4.2 训练命令与关键参数
训练入口一般长这样:
python src/train.py \ --train_file data/processed/train.json \ --val_file data/processed/val.json \ --model_name bert-base-uncased \ --batch_size 8 \ --epochs 3 \ --lr 3e-5 \ --max_seq_length 512 \ --output_dir outputs/batch_size=8是 8GB 显存下的保守值,显存够可以上 16。epochs=3是 BERT 微调的常见区间,再多容易过拟合。lr=3e-5在2e-5到5e-5之间都合理。max_seq_length要和预处理时一致,否则 padding 逻辑对不上。训练日志里重点看验证集的 F1,而不是准确率——负样本多的时候准确率虚高。
4.3 推理验证:先看输出再调参
训练完用predict.py对几篇验证集文档生成摘要,人工看一眼。如果摘要全是短句或者全是首句,说明标签构造或pos_weight有问题。建议先在小样本(20 到 50 篇)上跑通全流程,再上全量数据,这样排查成本低。
5. 避坑与排查:这几处最容易翻车
5.1 显存溢出:不是 batch 太大就是序列太长
现象是训练一开始就CUDA out of memory。原因通常是max_seq_length=512加上batch_size=16超过了显存。解决:先把batch_size降到 4 或 8,再考虑用梯度累积模拟大 batch。如果单句仍然超长,预处理阶段就把超过 512 token 的句子截断或再切分。
5.2 模型全预测 0:标签阈值和权重没调好
现象是验证集上模型对所有句子输出概率都接近 0,摘要为空。原因是正样本太少或pos_weight没设。解决:统计训练集正样本比例,如果低于 15%,把threshold降到 0.2,同时把pos_weight提到 3.0 到 5.0,观察正样本召回是否回升。
5.3 分句错误导致标签错位
现象是摘要里出现半截句子或跨段拼接。原因是sent_tokenize对缩写、小数点处理不好。解决:预处理时先做简单清洗(去掉多余换行、统一标点),中文场景直接换jieba或按。!?切分,别硬套英文分句器。
5.4 推理结果顺序混乱
现象是摘要句子读起来跳跃。原因是按概率排序后直接拼接,没恢复原文顺序。解决:选完 top-k 后按句子原始索引重排,这一步在 3.4 的代码里已经体现,但很多人自己改写时会漏掉。
5.5 版本不兼容导致 Trainer 报错
现象是TypeError: __init__() got an unexpected keyword argument。原因是transformers版本和代码里的Trainer参数不匹配。解决:按requirements.txt锁版本,或者干脆不用Trainer,自己写训练循环(3.3 的做法),可控性更强。
6. 二次开发:换模型、换标签、接业务
想在这个基线上继续做,有三个方向最值得动。第一是换编码器:把BertModel换成RobertaModel或中文的bert-base-chinese,只改model.py里一行,tokenizer 同步换掉即可,注意hidden_size从配置里读,别写死 768。第二是改标签策略:现在的词重叠阈值比较粗糙,可以换成 ROUGE-L 计算句子与参考摘要的匹配度,或者引入位置特征(首句、末句加权),把标签质量提上去,模型上限会明显不同。第三是接业务:把summarize函数包成一个 HTTP 接口,输入文档、输出摘要,前面加一层长度判断——短文档直接返回原文,长文档才走模型,这样能省掉大量无谓推理。
改造后的验证方法我一般固定三步:先用 20 篇人工标注样本算 ROUGE-1 和 ROUGE-L,和基线对比;再抽 10 篇看摘要可读性,重点看有没有断句和重复;最后压测一下单篇推理耗时,确认在业务可接受范围内。表格对比可以这样记:
| 改造点 | 改动文件 | 验证指标 |
|---|---|---|
| 换 RoBERTa | model.py | ROUGE-L 是否提升 |
| 换 ROUGE 标签 | preprocess.py | 正样本比例、F1 |
| 加长度短路 | predict.py | 平均耗时下降 |
有个习惯我保持了挺久:每次改标签构造逻辑,都先把新旧标签在同一批样本上的正负比例打出来对比,比例偏差超过 10% 就先不训练,回去查分句和阈值。因为标签一错,后面调参全是白费功夫,这个后悔药不好吃。希望帮到你。
本文还有配套的精品资源,点击获取