简介:这是一份面向本科毕业设计的深度学习文本摘要自动生成项目资源,适合自然语言处理方向的学生开展课题实践。资源以Transformer模型为核心,围绕数据预处理、模型构建、训练评估等完整流程展开,覆盖分词、词汇表构建、序列化输入、损失函数与优化器配置,并介绍ROUGE与BLEU评价指标的使用方法,同时涉及PyTorch或TensorFlow框架的工程实现。压缩包共34个文件,包含18个Python源码、5个Shell脚本、3个文本说明、2个YAML配置、词汇表文件及Docker部署相关文件,整体约360KB,目录结构清晰,便于按模块学习与调试。该资源已有3662人学习下载,其中模型定义、批处理逻辑、束搜索生成摘要、Web展示等代码可直接运行,适合作为毕业设计起点或NLP入门实战参考。
1. 基于深度学习的文本摘要自动生成:这届本科毕设为什么值得选它
每到毕业设计选题季,总会有人被“基于深度学习的文本摘要自动生成(自然语言处理)”这个题目吸引,又担心它太难。我的判断是,这个题目比图像分类更值得做,但也更容易翻车——难点不在模型,而在数据清洗和评估。简单说,任务是让模型读一篇长文本,输出一段能概括核心信息的短文本。它听起来是标准NLP任务,但一旦落到“自动生成”,你就是在让机器创作,而不只是分类。
本科毕设选这个方向有天然优势:既有理论深度,又有演示效果。适合谁做?有GPU或能借到算力,且愿意在数据清洗上花两周时间的人。接下来从路线选型讲到训练参数,再讲到评估的玄学,把一条能复现的路径完整摊开。
2. 摘要生成的路线选择:为什么毕设适合走生成式而不是抽取式
2.1 抽取式:半小时能跑的基线,但论文不好写
抽取式摘要的核心逻辑是从原文里挑几个句子拼成摘要。经典的TextRank算法不需要任何训练,把句子当节点、句子相似度当边,迭代算个PageRank,取分数最高的Top-K句子。
import re import numpy as np def textrank_summary(text, top_k=3): sents = [s for s in re.split(r'[。!?!?]', text) if len(s) > 5] tokens = [set(re.findall(r'[\u4e00-\u9fa5]', s)) for s in sents] n = len(sents) if n == 0: return "" sim = np.zeros((n, n)) for i in range(n): for j in range(n): if i != j: inter = len(tokens[i] & tokens[j]) union = max(len(tokens[i] | tokens[j]), 1) sim[i][j] = inter / union score = np.ones(n) for _ in range(10): score = sim.T @ score / max(n - 1, 1) ranked = sorted(range(n), key=lambda x: score[x], reverse=True) return "。".join([sents[i] for i in ranked[:top_k]])逻辑很简单:每个句子被映射成字级别集合,用Jaccard相似度算句子之间的关系,再做10轮类似随机游走的迭代,让被更多相似句子指向的句子得分更高。参数里值得注意的就两个:top_k控制摘要句子数,一般取3到5;迭代轮数10就够,再大分数不会明显变化。
这段代码可以作为基线写进论文的“对比实验”里。但作为毕设主模型,抽取式很尴尬:摘要里的每个字都来自原文,表达能力上限就摆在那,论文里的创新点很难写。你说你调了阈值、换了相似度度量,评委大概率会觉得工作量不够。
2.2 生成式:能写出原文没有的词,才是“自动生成”
生成式摘要走的是“编码器-解码器”路线:编码器把原文压成上下文向量,解码器逐字生成摘要。和抽取式最大的差别是,解码器每一步都在词表上算概率,所以它能产出原文里没有出现过的词和说法,甚至自己重组句子。这也是标题里“自动生成”四个字的含义所在。
这里最基础的结构是Seq2Seq加Attention。Attention的作用是让解码器在生成第i个词时,回看原文里哪些位置更重要,这直接改善了长文本场景下的信息丢失。到你真正做实验时,一般不会再从零训练这个结构,而是直接上预训练模型,效果和训练速度都更划算,后面第4章会展开。
从零训练Seq2Seq时,第一个要面对的问题是词表外词。摘要里经常出现人名、地名、数字组合,训练词表覆盖不住,解码时就会输出未登录词标记。常见处理方法是复制机制,把原文里出现的词直接拷贝到摘要里。这个概念写进论文很出彩,但实现起来要改动模型结构,对毕设来说性价比不高。预训练模型通过子词分词基本绕开了这个问题,这也是我推荐预训练微调的一个重要理由。
2.3 选型表:三条路线对比与毕设定位
常见的路线有三条:从零训练Seq2Seq、直接微调预训练模型、预训练为主线加从零模型做对比。
| 路线 | 训练成本 | 效果底线 | 论文工作量 | 翻车概率 |
|---|---|---|---|---|
| 从零训练Seq2Seq | 高,需要较多数据和调参 | 低,容易退化成复读机 | 高,要处理未登录词和词表 | 高 |
| 直接微调预训练模型 | 低,单卡也能跑 | 高,开箱即用 | 低,容易被评“调包” | 低 |
| 预训练为主+从零对比 | 中,训练两次模型 | 高 | 中,有对比有分析 | 中 |
我一般会推荐第三种,这也是答辩时最好讲的组合:用一个预训练模型做主线,证明深度学习方法能达到什么水平;再训练一个小的Seq2Seq做对照,证明注意力机制的贡献。对照实验的价值在毕业设计里被严重低估,它能帮你扛住“你只是套了个现成模型”这类提问。主模型规模大一点没关系,对比模型不用大,词表控制在2万以内,两层LSTM就够。
选择路线之前,先想清楚自己手里的显存。有24G显存,预训练微调随便跑;只有4G显存甚至用CPU,老老实实把序列长度压到256,训练轮数减半,或者直接把主模型换成小尺寸版本。显存大小决定你能跑多大的batch和max_length,这两个参数直接影响最终效果的稳定性。
3. 数据准备与预处理:清洗、切分与三个隐蔽问题
3.1 数据集的选型:公开数据优先,爬虫放最后
中文摘要方向,公开数据集中最常用的是“原文-摘要”成对的大规模语料,比如LCSTS这类中文短文本摘要数据集,也有一些比赛数据集自带官方划分。对毕设来说,优先选公开、带划分或容易自行划分的,尽量不要自己爬。自己爬语料听着很酷,但清洗噪声能吃掉两到三周,而且爬下来的正文质量参差不齐,最后模型效果差,你分不清是数据问题还是模型问题。
选数据集时还要看平均长度。新闻类数据原文长、摘要长,适合做生成式;微博类数据原文短、口语化重,摘要经常只有一句话,模型很容易直接拷贝。毕设建议选“原文100到300字、摘要20到80字”这个区间,和真实场景更贴近,也更容易做出可见的压缩效果。
3.2 清洗脚本:四个正则和一个长度过滤
拿到原始数据后,第一件事不是训练,而是把脏数据清掉。下面这套清洗逻辑是文本摘要任务我每次都会先跑一遍的框架。
import re def clean_pair(src, tgt): # 去掉控制字符和不可见字符 src = re.sub(r'[\x00-\x08\x0b-\x1f\x7f]', '', src) tgt = re.sub(r'[\x00-\x08\x0b-\x1f\x7f]', '', tgt) # 统一换行为空格,避免切句错乱 src = re.sub(r'\s+', ' ', src).strip() tgt = re.sub(r'\s+', ' ', tgt).strip() # 全角逗号句号换半角 src = src.replace(',', ',').replace('。', '.') tgt = tgt.replace(',', ',').replace('。', '.') # 长度过滤 if len(src) < 20 or len(src) > 300: return None, None if len(tgt) < 8 or len(tgt) > 80: return None, None # 摘要不能和原文一模一样 if src == tgt: return None, None return src, tgt这段脚本里最容易被忽略的是全角转半角。ROUGE评估按字符匹配,如果你把“,”和“,”当成两个不同字符,模型得分会凭空低好几个点;反之,如果你评估前不统一标点,得分又会虚高。统一标点是给训练和评估省掉后续麻烦的一件事。
长度过滤为什么重要?因为摘要任务本质是一个压缩率问题。原文20个字,摘要也20个字,模型直接学拷贝;原文300字,摘要却只有8个字,信息密度太高,模型学不动。把训练样本控制在“原文中等长度、摘要中等长度”区间,模型最容易收敛。这个比例不是玄学,是经验值,你可以根据自己数据的分布微调过滤边界。
3.3 切分数据:随机种子与“去泄漏”切分
切分同样有讲究。我一般按90%训练、5%验证、5%测试来切,固定随机种子,保证每次实验结果可比。
import random pairs = [] # 元素是 (src, tgt),已经过 clean_pair 过滤 random.seed(42) random.shuffle(pairs) total = len(pairs) train = pairs[:int(total * 0.9)] valid = pairs[int(total * 0.9):int(total * 0.95)] test = pairs[int(total * 0.95):]这里有一个隐蔽问题:如果你的数据来自同一个专栏或同一个作者,随机切分会让同一篇文章的不同段落同时出现在训练和测试里,测试分数虚高。处理办法是按来源ID分组后切分,而不是按行切分。公开数据集如果自带分组信息,优先按分组信息切;如果没有,就接受随机切分并在论文里说明局限。
另一个隐蔽问题是缓存。训练时很多框架会做动态padding,把同一个batch内的样本垫到相同长度,这本身没问题。但如果你用了datasets库的map函数,记得关闭缓存开关,否则你清洗过一次的数据会被缓存,改清洗逻辑后不会自动更新,你想当然以为数据变了,其实还在用旧缓存。这是排查半天效果不变时最先怀疑的地方。
4. 模型训练与生成:预训练微调参数清单和解码玄学
4.1 环境与最小复现依赖
跑预训练微调,最常见的做法是装好PyTorch和Transformers库,数据加载用datasets库。
pip install torch transformers datasets rouge-score版本这块我给不出一个对所有人都成立的组合,但建议三个库在同一时间点装好,避免API变动导致网上抄来的代码报错。下面代码里的AutoModelForSeq2SeqLM适用于T5、PEGASUS这类编码器-解码器结构。中文摘要方向,优先选中文语料预训练的权重,直接拿英文权重微调中文任务,效果大概率不理想。
4.2 加载模型与数据整理:把数据变成模型能吃的格式
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM checkpoint = "你的中文摘要预训练权重" # 选一个中文T5或PEGASUS的小尺寸权重 tokenizer = AutoTokenizer.from_pretrained(checkpoint) model = AutoModelForSeq2SeqLM.from_pretrained(checkpoint) def preprocess(example): src = tokenizer( example["src"], max_length=512, truncation=True, padding=False ) tgt = tokenizer( example["tgt"], max_length=128, truncation=True, padding=False ) src["labels"] = tgt["input_ids"] return src这里有个细节:labels直接用目标摘要的input_ids,不需要额外封装。训练中如果出现loss为0但生成全是乱码,先查labels有没有被padding到正确位置。部分教程会把pad token对应的位置设成-100,这不是必须的,只要损失函数在计算时能自动忽略padding位置就行。
max_length=512和max_length=128是原文和摘要各自的上限。原文512个token对中文300字够用;摘要128个token对应约80字,留一点余量。如果你的数据长度分布偏短,可以再压到256和64,显存占用会明显下降。
4.3 训练参数:每个数值都要能解释给评委听
from transformers import Seq2SeqTrainingArguments, Seq2SeqTrainer args = Seq2SeqTrainingArguments( output_dir="./sum_model", learning_rate=3e-5, per_device_train_batch_size=16, per_device_eval_batch_size=32, num_train_epochs=5, weight_decay=0.01, evaluation_strategy="epoch", save_strategy="epoch", predict_with_generate=True, generation_max_length=128, generation_num_beams=4, save_total_limit=2, seed=42, ) trainer = Seq2SeqTrainer( model=model, args=args, train_dataset=train_dataset, eval_dataset=valid_dataset, tokenizer=tokenizer, ) trainer.train()参数说明的快读版:learning_rate=3e-5是预训练微调的标准量级,比这个高容易灾难性遗忘,学不到新任务还丢了预训练知识;per_device_train_batch_size取决于显存,16是小模型常见值,显存不够就减半并用梯度累积把有效batch补回来;num_train_epochs=5对中文摘要一般够用,验证集分数不再涨就该停;generation_num_beams=4是速度和质量的折中点。
这里插一句容易踩的坑:predict_with_generate=True表示评估阶段用生成模式,而不是teacher forcing。如果你设成False,验证loss可能是降的,但生成的摘要照样稀烂,因为这个loss衡量的是“给定正确答案猜下一个词”,和“完全自己生成”是两回事。这个开关答辩时也常被问,要能说清楚两者的区别。
提示:如果开着 predict_with_generate,评估阶段会自动运行生成逻辑。generation_num_beams 和 generation_max_length 只在评估和预测时生效,训练阶段波束搜索不参与梯度计算。
4.4 解码参数:beam size、重复惩罚和长度惩罚的直觉
训练完模型,生成阶段的参数比训练参数更影响观感。同一个checkpoint,解码参数不同,摘要质量可能是两个级别。
generated = model.generate( **batch, max_length=128, num_beams=4, no_repeat_ngram_size=3, length_penalty=1.0, early_stopping=True, )num_beams是每步保留的候选序列数,越大越接近全局最优,但速度变慢且更容易“一本正经地重复”。no_repeat_ngram_size=3表示任何三个连续词不能重复出现,这是治复读机最有效的参数,如果你的摘要里反复出现同一个短语,先调它。length_penalty小于1会惩罚长句、让摘要更短;大于1则鼓励长句。新闻摘要一般从1.0起步,发现摘要普遍偏短再往0.9压,偏长就往1.1调。
解码参数在毕业论文里也很适合做消融实验:固定住训练好的模型,分别测beam size为1、2、4时的ROUGE,再测no_repeat_ngram开关前后的重复率。这套实验不需要重新训练,半天就能做完,却能撑起论文里“生成策略分析”一整节。
5. 避坑:摘要模型最容易翻车的五个问题与排查路径
5.1 复读机:loss在降,ROUGE不涨
现象:训练loss一路下降,验证集ROUGE始终卡在20出头;生成的摘要反复出现“该用户表示”“记者了解到”这类局部重复。
原因:beam search的目标是整句概率最高,但如果没有局部重复惩罚,模型会因为高频词相互吸引而不停重复,陷入局部最优。这本质上是解码策略问题,不是欠拟合。
解决:生成阶段开启no_repeat_ngram_size=3;如果还不行,把beam size从4降到2,并检查训练数据里是不是有大量模板句,让模型把“模板惯性”学会了。两个方向同时调,一般两三轮实验就能看到明显改善。
5.2 摘要全是套话:领域单一化
现象:模型生成的摘要放哪篇文章上都成立,比如“某公司召开会议讨论今年工作”,整句话没有信息量。
原因:训练集领域太集中,模型学会了高频词汇的高概率,却没学会“针对特定文章找独特信息”。新闻摘要数据集里如果全是某一种类型的内容,这个问题会特别明显。
解决:检查训练集来源,按话题或来源做分组,保证测试集不跟训练集同属一个专栏或同一时间窗口;如果数据量足够,加入另一批不同风格的数据做混合训练。论文里可以把“领域多样性对摘要信息量的影响”作为一个小实验来写。
5.3 评估虚高:标点和前导词泄漏
现象:ROUGE-1有40多,感觉论文要成了;人工看一眼摘要,发现全是抽取出来的原文句子,根本不是生成。
原因:评估时没做预处理,标点、数字、专名全被当成普通字符参与匹配;如果摘要和原文都带“本报讯”这类固定前导词,ROUGE直接这些死词刷高。
解决:评测前先统一走一遍清洗,去标点、去停用词、去掉固定前导;同时把“摘要里有多少比例的内容与原文重复”作为第二个报告指标,防止只看ROUGE被表面数字骗过去。这个指标不需要额外库,字符串匹配就能算。
5.4 显存溢出:batch与max_length打架
现象:训练到第二个epoch,CUDA out of memory。
原因:per_device_train_batch_size和max_length同时设得太大,激活值把显存吃满了。16的batch配合512的max_length,在显存小的卡上一定会炸。
解决:先降max_length到256,再降batch size到8或4;降到合理范围后,用梯度累积把有效batch size补回来,保证收敛稳定性。建议把这两个参数单独写在配置区,调显存时只改配置,不要每次改代码里多个地方,容易改漏。
5.5 结果不稳定:同样的代码两个分数
现象:训练两遍,同一套验证集,ROUGE差3到5分。你以为是模型随机的“玄学”,其实是实验习惯问题。
原因:没固定随机种子,数据打乱顺序、dropout、初始化都有随机性;小数据集上这种波动会被放大。
解决:固定seed=42并保持全流程一致;训练完保存checkpoint,评估用同一个checkpoint而不是重新训练。把“固定随机种子”写进毕业设计的实验环境说明里,答辩时是一个加分的细节,说明你关注实验结果的可复现性。
6. 验证与进阶:ROUGE之外的习惯,和三个能写进论文的方向
6.1 ROUGE怎么算,才算没算错
ROUGE-1是单字重合率,ROUGE-2是二元组重合率,ROUGE-L是最长公共子序列的F值。它们衡量的是“参考摘要里的信息有没有被覆盖”,但不衡量“你多出来的内容是否错误”。用rouge-score库几行就能算:
from rouge_score import rouge_scorer scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"], use_stemmer=True) scores = scorer.score(gold_text, pred_text)对中文,记得确保分词和标点已统一,否则你复现不出别人的分数。论文里报三个指标的同时,一定要做bad case分析,把验证集的输出分成漏掉关键信息、保留冗余内容、指代错误、重复碎片几类。这个分类本身就能撑起论文里“结果分析”一节。
6.2 三个值得做的改进方向
第一个是按压缩比调整训练样本分布。你可以在采样时按原文长度区间做重采样,让模型见过更多不同的压缩比,这能直接改善“短摘要信息密度过大”的问题。
第二个是注意力可视化。把解码器生成重点词时的attention权重存下来,画几个热力图,放在论文里非常能说明模型内部行为。实现上只需要在生成时打开output_attentions开关,不需要改模型结构。
第三个是关键词引导生成。用一个关键词抽取器先从原文里抽三个词,拼到输入末尾,让模型生成时更容易聚焦核心信息。这个改动很小,效果提升却肉眼可见,我见过不少毕设靠这个方向多写出一节实验。
做这套东西最有用的一个习惯是:每次训练都记录“模型版本、参数、数据统计、验证ROUGE、bad case截图”五件套,不要只存模型。我毕业设计时吃过一次亏:一个调了很久的参数组合,没记录当时的batch size和随机种子,后来想复现那个分数,花了两天重新试。从那以后,任何摘要实验我都会先建一个实验记录表。希望这个习惯也能帮到你,让你把时间花在模型改进,而不是重新找参数上。
本文还有配套的精品资源,点击获取