简介:一套基于深度学习的文本摘要自动生成完整项目方案,面向本科毕业设计或NLP入门实践者。项目以Transformer编码器-解码器架构为核心,完整覆盖文本预处理、模型构建、训练、评估与测试全流程,并包含beam search解码、ROUGE/BLEU指标评测等关键环节,能够帮助读者系统掌握自动摘要生成的核心技术。资源包共34个文件,体积仅360KB,内部以18个Python脚本和5个Shell脚本为主体,配合配置文件、词表、Dockerfile、依赖清单等,便于快速搭建可复现的实验环境,也支持本地与Docker两种运行方式。目前已有3700余人学习下载,代码结构清晰,尤其适合希望动手实践Transformer模型的本科生;通过运行训练与测试脚本,读者可完整走通从原始文本到生成摘要的流程,并借助评测指标持续优化模型效果,为后续NLP研究或毕业设计答辩打下扎实基础。
1. 基于深度学习的文本摘要自动生成:毕业设计真能做出来,但坑比你想的多
文本摘要这个方向,很多初学者以为就是“提取关键词拼一段话”,但真正用深度学习做生成式摘要后会发现,它本质上是“理解原文后重新组织语言”的过程,和你用 Word 的自动摘要完全是两回事。这套基于深度学习与自然语言处理的本科毕业设计资源,核心是用 Transformer 架构实现从新闻长文到短摘要的自动生成,包含数据处理、模型训练、推理和评估的完整闭环。适合选了 NLP 方向但还没定题、或者定了文本摘要但不知道从哪下手的本科生。我拆完这套资源后最直观的感受是:跑通一个 BART 摘要模型不难,难的是把数据集清洗干净、把解码参数调对、把指标看懂,这几件事做完,论文的核心工作量也就立住了。
2. 摘要任务先定性:抽取式还是生成式,决定你后面一半的工作量
2.1 抽取式和生成式的本质区别与选型判断
文本摘要任务分成两大类。抽取式是从原文里挑出重要句子拼在一起,本质是“排序+截取”;生成式是模型重新写出一段话,虽然信息来自原文,但措辞是新的。本科毕设选哪个方向,直接影响你的硬件需求、模型复杂度和论文工作量。
表格对比一下:
| 维度 | 抽取式(Extractive) | 生成式(Abstractive) |
|---|---|---|
| 实现难度 | 低,TextRank 即可 | 高,需要 Transformer 类模型 |
| 训练资源 | CPU 可跑 | 需要 GPU,哪怕是入门级 |
| 摘要流畅度 | 一般,句子拼接感强 | 接近人写,表达更自然 |
| 本科毕设适配度 | 偏简单,容易被质疑工作量 | 适中,深度学习特征明显 |
| 评估指标 | ROUGE-1/2,容易偏高 | ROUGE-1/2/L 均需关注 |
这套资源走的是生成式路线,模型用 Transformer 家族。我个人建议,如果你的毕设导师比较看重“深度学习”这个标签,就老老实实做生成式,纯抽取式的 TextRank 写进论文里只能算前置实验,不能作为主模型。但反过来,如果你的显卡只有显存 4GB 以下,生成式会很痛苦,要么用蒸馏后的小模型,要么把最大长度压得很短,后面我会讲怎么处理。
2.2 数据集预处理:摘要任务真正消耗时间的地方
很多人拿到数据集就直接 train 了,这是最大的错误。摘要任务的数据集处理比分类任务讲究得多,核心有三件事:清洗、长度过滤、划分。
清洗阶段要去掉 HTML 标签、乱码、连续空格,还要处理数据里的特殊字符比如全角引号。常见做法是用正则表达式统一清洗。长度过滤是为了防止训练时 Padding 太长浪费显存,也防止过短的样本让模型学到无意义映射。我一般会设置原文长度在 100 到 800 词之间,摘要长度在 10 到 128 词之间,超出范围的样本直接丢弃或用 3 倍下采样。
下面是这套资源里给的数据预处理脚本核心逻辑,语言是 Python:
import re import json def clean_text(text): # 去掉 HTML 标签:摘要数据里最常见的污染源 text = re.sub(r'<[^>]+>', '', text) # 将多个连续空白符压缩为单个空格 text = re.sub(r'\s+', ' ', text) # 统一全角逗号和句号为半角 text = text.replace(',', ',').replace('。', '.') return text.strip() def filter_by_length(src, tgt, max_src=800, min_src=100, max_tgt=128, min_tgt=10): # 按词数过滤,这里用空格分词,中文场景建议用 jieba 提前切好 src_len = len(src.split()) tgt_len = len(tgt.split()) if src_len < min_src or src_len > max_src: return False if tgt_len < min_tgt or tgt_len > max_tgt: return False return True def build_dataset(raw_path, out_path): with open(raw_path, 'r', encoding='utf-8') as f: lines = f.readlines() samples = [] for line in lines: data = json.loads(line) src = clean_text(data['document']) tgt = clean_text(data['summary']) if filter_by_length(src, tgt): samples.append({'src': src, 'tgt': tgt}) with open(out_path, 'w', encoding='utf-8') as f: for s in samples: f.write(json.dumps(s, ensure_ascii=False) + '\n') print(f'raw={len(lines)}, kept={len(samples)}, ratio={len(samples)/len(lines):.2f}')这段代码的逻辑顺序是:先清洗再过滤,最后写入文件。清洗必须在过滤之前,否则带标签的文本长度统计会失真。filter_by_length 里的阈值不是拍脑袋定的,BART 和 T5 这类模型的最大输入长度通常是 512 到 1024 token,如果原文超过 800 词,实际切分后能用的 token 也会超过模型上限,就算勉强塞进去,注意力计算也会变得很稀疏。另外注意,这里用 split() 按空格分词只适用于英文,如果是中文数据集,建议先按字符级处理,或者用 jieba 分词后再过滤,否则长度统计会偏差很大。
3. 抽取式 Baseline 先跑通:TextRank 实现与 ROUGE 评估的对应关系
3.1 用 TextRank 做基线模型,不是为了用,是为了“陪跑”
毕设论文里需要对比实验,生成式模型再先进,也需要一个基线来衬托。TextRank 是抽取式里最经典的算法,理论上不需要训练,无监督就能跑,非常适合当基线。这套资源里包含 TextRank 的完整实现,我拆完代码后发现它用了标准的图排序思路:句子是节点,句子间的相似度是边权重,然后迭代计算句子权重,最后取 Top-K 句拼成摘要。
关键代码结构如下:
import numpy as np from sklearn.metrics.pairwise import cosine_similarity import networkx as nx def textrank_summarize(sentences, top_k=3): # 句子向量化:这里用 TF-IDF,简单且不需要额外模型 vectorizer = TfidfVectorizer(stop_words='english') X = vectorizer.fit_transform(sentences) # 计算句子间余弦相似度矩阵,作为图的邻接矩阵 sim_mat = cosine_similarity(X) np.fill_diagonal(sim_mat, 0) # 构建无向加权图并运行 PageRank 迭代 graph = nx.from_numpy_array(sim_mat) scores = nx.pagerank(graph, alpha=0.85) # 按得分排序取前 top_k 个句子,并按原始顺序输出 ranked = sorted(((scores[i], i) for i in range(len(sentences))), reverse=True) selected = sorted([idx for _, idx in ranked[:top_k]]) return ' '.join(sentences[i] for i in selected)这里有个容易被忽略的细节:选出来的句子要按原始顺序重新排序,否则摘要读起来是乱的。很多入门实现直接按得分降序输出,结果摘要第一句是原文的第五句,第二句是原文的第二句,语句完全不通。我建议你在论文里明确写清楚这一步,这属于抽取式摘要的经典实现细节,能体现你对任务的理解。
参数方面,alpha=0.85 是 PageRank 的阻尼系数,一般不用改。top_k 是摘要句子数量,要根据原文长度和目标摘要字数调整,常见做法是原文句子数的 30% 左右,或者直接固定为 3 到 5 句。
3.2 ROUGE 指标的计算方式和阅读方法
做摘要方向的毕设,ROUGE 是绕不开的评估指标。ROUGE-1 比较单字/单词重叠,ROUGE-2 比较相邻两个词的重叠,ROUGE-L 比较最长公共子序列。这套资源里给了 ROUGE 的计算脚本,代码不算长,核心是精确率、召回率和 F1 的权衡问题。
from rouge_score import rouge_scorer scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True) def evaluate_summary(pred, ref): scores = scorer.score(ref, pred) print(f'ROUGE-1 F1: {scores["rouge1"].fmeasure:.4f}') print(f'ROUGE-2 F1: {scores["rouge2"].fmeasure:.4f}') print(f'ROUGE-L F1: {scores["rougeL"].fmeasure:.4f}') return scores注意参数顺序,scorer.score 的第一个参数是参考摘要 ref,第二个是生成摘要 pred,很多人传反,结果指标完全无意义。ROUGE 的召回率表示参考摘要里有多少内容被生成摘要覆盖了,精确率表示生成摘要里的内容有多少来自参考摘要,F1 是两者的调和平均。写论文时一般报告 F1,但审稿老师如果仔细看,你还需要解释为什么只报 F1 而不分析精确率和召回率的差异。
这里有一个很现实的注意点:ROUGE 是基于词面重叠计算的,如果模型把“苹果”改写成了“水果”,哪怕语义完全正确,ROUGE 分数也很低。这导致生成式模型的 ROUGE 分数往往比抽取式模型低,这是正常的。你的论文讨论部分必须解释这个现象,否则答辩老师会质疑“你的深度学习模型怎么还没传统算法分数高”。这个资源里点到了这一点,我觉得很有价值。
4. 生成式主模型:BART 的微调训练与推理参数详解
4.1 为什么选 BART 而不是 T5 或 GPT
这套资源的主模型是 BART,不是 T5。原因很直接:BART 是专门为生成任务设计的去噪自编码模型,预训练时就做了文本损坏恢复,这和摘要任务高度同构——摘要本质上就是把“长文压缩成短文”的文本变换任务。而 T5 是 Text-to-Text 架构,虽然也适合摘要,但同等规模下 BART 在 ROUGE 指标上通常略优。GPT 类模型做摘要需要 Prompt 工程,对本科毕设来说不稳定因素太多,不推荐。
BART 的训练方式是在源文本上做 Denoising 预训练,比如 Token 掩码、句子顺序打乱、文本删除等。微调时,我们把原文作为输入序列,把目标摘要作为输出序列,训练目标是最小化生成摘要和参考摘要之间的交叉熵损失。资源里用的模型权重是 BART-base,也就是 6 层编码器加 6 层解码器,参数量约 1.4 亿,单卡 8GB 显存勉强够用。
4.2 训练脚本核心参数与数据加载方式
训练阶段最核心的类有两个,一个是数据集类,另一个是训练循环。下面这段代码是资源里的数据加载部分,我把关键参数做了注释:
from torch.utils.data import Dataset from transformers import BartTokenizer, BartForConditionalGeneration class SummarizationDataset(Dataset): def __init__(self, data_path, tokenizer, max_src_len=512, max_tgt_len=128): self.samples = [json.loads(line) for line in open(data_path, encoding='utf-8')] self.tokenizer = tokenizer self.max_src_len = max_src_len self.max_tgt_len = max_tgt_len def __len__(self): return len(self.samples) def __getitem__(self, idx): sample = self.samples[idx] # 原文编码:截断到 max_src_len,这里不用 padding,交给 collator 统一处理 src_ids = self.tokenizer( sample['src'], max_length=self.max_src_len, truncation=True, return_tensors='pt' ).input_ids.squeeze(0) # 目标摘要编码:同样截断到 max_tgt_len tgt_ids = self.tokenizer( sample['tgt'], max_length=self.max_tgt_len, truncation=True, return_tensors='pt' ).input_ids.squeeze(0) return {'input_ids': src_ids, 'labels': tgt_ids}这里有一个重要的参数细节:max_src_len 和 max_tgt_len 分别限制原文和摘要的长度。如果你显存有限,把 max_src_len 从 512 降到 256,训练速度会快很多,但摘要质量会下降,因为模型看不到文章后半部分的信息。资源里的默认设置是 512 和 128,硬件条件好的话可以保持,如果显存吃紧,建议优先缩减训练 batch size 而不是收缩长度。
训练循环中需要注意 labels 的处理。Hugging Face 的 Trainer 会自动将 labels 中的 -100 位置忽略掉,但手动写训练循环时,你需要把 padding token 的 id 替换为 -100,否则模型会学习预测填充符,降低训练质量。下面是资源里的处理方式:
def collate_fn(batch): input_ids = [item['input_ids'] for item in batch] labels = [item['labels'] for item in batch] # padding 时 input_ids 和 labels 都填充到 batch 内最大长度 input_ids_padded = torch.nn.utils.rnn.pad_sequence(input_ids, batch_first=True, padding_value=tokenizer.pad_token_id) labels_padded = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True, padding_value=-100) return {'input_ids': input_ids_padded, 'labels': labels_padded}pad_sequence 默认按 batch 内最长样本补零,但这里 padding_value 必须区分开:输入侧用 tokenizer 的 pad_token_id,标签侧用 -100。如果两边都用 0 补,损失函数会计算填充位置的概率,模型会学偏。这个细节我当年调了两天才发现,属于典型的“看不出来但影响巨大”的坑。
4.3 推理阶段:beam search 与 length penalty 的调参经验
推理阶段是生成式摘要的另一个调参重灾区。资源里给的生成函数使用了 beam search,核心参数有三个:num_beams、length_penalty、early_stopping。下面这段代码可以直接用作测试脚本:
from transformers import BartTokenizer, BartForConditionalGeneration model = BartForConditionalGeneration.from_pretrained('./bart-summarization-checkpoint') tokenizer = BartTokenizer.from_pretrained('./bart-summarization-checkpoint') def generate_summary(text, model, tokenizer): inputs = tokenizer( text, max_length=512, truncation=True, return_tensors='pt' ) summary_ids = model.generate( inputs.input_ids, num_beams=4, length_penalty=2.0, max_length=128, min_length=20, early_stopping=True, no_repeat_ngram_size=3 ) return tokenizer.decode(summary_ids[0], skip_special_tokens=True)num_beams 控制搜索宽度,4 是性价比最高的选择,继续增加到 8 会显著减慢推理速度,但 ROUGE 提升很小。length_penalty=2.0 的意思是鼓励生成长一点的摘要,如果设为 1.0 或更低,模型容易输出很短的内容,尤其在没有 min_length 约束时会更明显。no_repeat_ngram_size=3 是防止生成重复短语的约束,这个参数对摘要质量影响很大,不加的话模型在长摘要里经常会陷入重复循环,比如反复输出“某某表示某某表示”。我建议你测试时至少对比 length_penalty 在 1.0 到 2.5 之间的表现差异,论文里可以放一个小的调参表格,这是很加分的工作量。
推理阶段还有个常被忽略的问题:beam search 输出的摘要可能存在反复重复的 n-gram,加上 no_repeat_ngram_size 可以解决。但另一个现象更隐蔽——模型在解码初期容易出现“模型还没看完原文就开始输出”的问题,这属于注意力分布异常,通常和训练数据中摘要与原文长度比有关,一般是加长训练轮次或调整学习率来缓解,不是推理阶段能解决的。
5. 实战避坑指南:训练摘要模型最容易踩的六个常见问题
5.1 ROUGE 分数一直不涨,先检查数据清洗和长度映射
现象:训练了多个 epoch,验证集 ROUGE-2 分数始终在 10 以下,而且不随时间波动。原因:大概率是原始数据里 HTML 标签没有被清除,或者原文和摘要的对应关系错位,模型等于在学习噪声。解决:用本章第二节的清洗逻辑重新处理数据,并且随机抽 50 条样本人工检查原文是否与摘要匹配。我自己拆的这套资源里,数据清洗脚本里有一个细节处理:把摘要中的换行符统一替换成空格,防止模型学到换行位置作为截断信号。
5.2 模型输出大量重复片段
现象:生成的摘要出现连续重复词,比如“该研究该研究该研究”或者一个句子重复三次。原因:解码时没有加 no_repeat_ngram_size,再加上 beam search 的早期解码错误会不断自我强化。解决:在 generate 参数中加上 no_repeat_ngram_size=3,如果仍然重复,把 beam 宽度从 4 减到 3,减少搜索分支的累积误差。
5.3 显存溢出(CUDA out of memory)
现象:训练刚开始就报显存不足。原因:max_src_len 过长,batch size 太大,或者模型用的是 BART-large。解决:优先把 batch size 从 8 降到 4,如果还不行就把 max_src_len 从 512 降到 256。注意,降 max_src_len 会改变训练数据的分布,需要重新统计样本长度,不能盲目全局截断。另外检查是否开启了梯度累积,batch size 小的场景建议用 gradient_accumulation_steps=2 来模拟更大的有效批量。
5.4 用验证集调参导致指标虚高
现象:验证集 ROUGE 很好,但测试集表现明显下降。原因:反复用验证集做早停和调参,验证集信息已经泄漏到模型选择过程中。解决:把数据集划分成训练、验证、测试三份,验证集只用于早停,所有最终指标在测试集上计算一次。资源里的数据划分比例是 8:1:1,并且用了固定随机种子,这样重复实验时结果可复现,论文里可以写清楚。
5.5 BLEU 还是 ROUGE?评估指标没选对
现象:论文里用了 BLEU 指标,但摘要场景下 BLEU 不能反映内容覆盖度。原因:BLEU 是机器翻译指标,强调译文和参考译文的 n-gram 精确匹配,对摘要任务来说,它会惩罚那些表述不同但语义正确的摘要。解决:摘要任务统一用 ROUGE 家族指标,主报告 ROUGE-1、ROUGE-2、ROUGE-L 的 F1 值。如果你想增加说服力,可以额外报告人工评估的流畅度和忠实度打分,这个比任何自动指标都有说服力。
5.6 中文数据集的 Token 切分问题
现象:中文摘要训练时,ROUGE 分数很低,但看生成的摘要好像还行。原因:ROUGE 默认按空格分词,中文如果不分词,所有中文文本会被当成一个超长 token,指标计算失去意义。解决:中文摘要数据要先用 jieba 分词,再用空格连接后训练和评估。资源里这段处理写在数据预处理脚本里,如果你自己处理中文数据,务必在清洗后加这一步。
提示:上面这些坑基本都是摘要任务特有的,分类或序列标注任务里不会遇到。如果你在训练过程中遇到了以上没有覆盖的问题,优先检查数据管道,不要急着调模型结构。
6. 写出能进论文的摘要评估方案:ROUGE 之外的验证技巧
模型训练完,不能只贴一个 ROUGE 表格就完事。答辩老师大概率会问“你生成的摘要质量到底怎么样”,如果你只拿出 ROUGE 分数,很难让老师信服。我建议在论文里加入人工评估方案,这是资源里也特别强调的一步。
人工评估通常从三个维度打分:信息忠实度(摘要内容是否在原文中有依据,满分 5 分)、流畅度(读起来是否通顺,满分 5 分)、信息覆盖度(原文核心信息是否都出现在摘要中,满分 5 分)。找三个同学或者同组成员,每人抽 50 条生成摘要,独立打分,然后计算平均分和一致性。具体做法是:从测试集随机抽 50 条原文,用模型生成摘要,再把原文、参考摘要、模型摘要三样放在一起,让评估者给模型摘要打分。如果有多个人打分,计算一下肯德尔和谐系数来验证评价一致性,这个在论文的方法部分写出来非常有说服力。
另外还有一个值得在论文中提的实验设计:解码参数敏感性分析。固定模型权重,只改变 num_beams 和 length_penalty,给出一个 3x3 的 ROUGE 分数网格。比如 num_beams 取 1(等价于贪心搜索)、4、8,length_penalty 取 1.0、2.0、2.5,统计每个组合在测试集上的 ROUGE-L 分数。你会发现贪心搜索的 ROUGE 通常比 beam search 低不少,这就是你在讨论部分解释 beam search 有效性的直接证据。这个实验成本很低,跑一次推理的时间就能完成,但工作量看起来非常扎实。
我还养成了一个习惯,现在每次跑完摘要模型,都会额外做一个“去停用词再算 ROUGE”的验证:把生成摘要和参考摘要里的停用词删掉再算 ROUGE。这个指标滤掉了虚词的干扰,更能反映信息层面的覆盖情况,虽然不能作为正式指标写进论文,但自己判断模型质量时很有效。如果去停用词后 ROUGE 明显提升,说明模型学到了关键信息但措辞偏冗余,反过来如果提升不大,说明模型的内容覆盖确实有问题。
从那以后,我每次做完摘要实验,都强制走一遍“清洗数据统计抽样、跑抽取式基线、微调 BART、beam search 调参、ROUGE 加人工评估”这条完整流程,缺一步都会在答辩时暴露问题。这套资源把整条链路串起来了,对本科毕设来说足够撑起工作量。希望帮到你。
本文还有配套的精品资源,点击获取