简介:本资源为基于深度学习的文本摘要自动生成本科毕业设计项目,面向自然语言处理方向的学生与初学者,帮助其掌握从长文本中提取关键信息并生成精炼摘要的完整流程。项目以Transformer模型为核心,涵盖文本预处理、分词、词汇表构建、序列化输入、模型训练与性能评估等环节,并引入ROUGE、BLEU等指标衡量摘要质量。压缩包共34个文件,约360KB,包含18个Python脚本、5个Shell脚本、3个文本文件、2个YAML配置、1个词汇表文件及README、Dockerfile、LICENSE等,覆盖模型定义、数据工具、训练与评估脚本、Web演示及容器化部署配置,结构清晰便于按模块学习。已有3764人学习下载,适合作为毕业设计参考或NLP入门实践素材,读者可借此理解自注意力机制与编码器-解码器架构,并提升深度学习框架的编程与调试能力。
1. 从一份“摘要生成器”毕设说起:为什么选 Transformer 而不是 LSTM
带过几届本科毕设之后,我发现一个规律:凡是做自然语言处理方向的学生,十有八九会选文本摘要自动生成这个题目。原因很实在——需求清晰、数据公开、效果肉眼可见,答辩时也好演示。但真正动手之后,翻车的地方往往不在模型本身,而在方案设计的第一步就埋下了隐患。
这份本科毕业设计围绕“基于深度学习的文本摘要自动生成”展开,核心任务是把一段长文本压缩成保留关键信息的短文本。技术栈锁定在深度学习与自然语言处理,关键词里明确出现了 Transformer。适合两类人参考:一是正在做 NLP 方向毕设、需要一套完整方案框架的学生;二是想快速搭一个摘要生成原型、验证业务可行性的从业者。整份方案从数据预处理、模型选型、训练策略到评估指标,形成一条可复现的链路,不是只跑一个 demo 就交差。
我一般会先问一句:你是做抽取式还是生成式?这个问题决定了后面所有模块的设计走向。抽取式从原文挑句子拼成摘要,实现简单、语法几乎不会出错,但灵活性差;生成式自己组织语言,效果上限高,但训练难度和评估复杂度都上一个台阶。这份毕设选择生成式路线,用 Transformer 作为骨干网络,下面把整套方案拆开讲。
2. 需求分析与技术选型:生成式摘要的四个核心决策点
2.1 任务定义与输入输出规范
文本摘要自动生成,形式化地说,是给定输入序列 X = {x₁, x₂, …, xₙ},输出摘要序列 Y = {y₁, y₂, …, yₘ},其中 m 远小于 n。生成式摘要不要求 Y 中的每个词都来自 X,模型需要理解原文语义后用自己的话重新表达。
输入侧通常设定最大长度 512 个 token,超出部分截断。这个数字不是拍脑袋定的——Transformer 的注意力计算复杂度是 O(n²),512 是显存和效果之间比较平衡的取值。输出侧摘要长度一般限制在 100~150 个 token,对应中文大约 60~100 字。训练时用 teacher forcing,推理时用自回归逐词生成。
数据格式上,我习惯把原始语料整理成 JSON Lines,每行一条样本,包含source和target两个字段。这样做的好处是加载快、易分片、方便做数据清洗。
import json def build_dataset(raw_path, save_path, max_src_len=512, max_tgt_len=150): """ 将原始语料转换为 JSONL 格式 raw_path: 原始数据路径,每行格式为 "原文\t摘要" save_path: 输出路径 max_src_len: 原文最大 token 数 max_tgt_len: 摘要最大 token 数 """ with open(raw_path, 'r', encoding='utf-8') as fin, \ open(save_path, 'w', encoding='utf-8') as fout: for line in fin: parts = line.strip().split('\t') if len(parts) != 2: continue # 跳过格式异常的行 src, tgt = parts[0].strip(), parts[1].strip() if len(src) < 50 or len(tgt) < 10: continue # 过滤过短样本,避免噪声干扰 record = { "source": src[:max_src_len], "target": tgt[:max_tgt_len] } fout.write(json.dumps(record, ensure_ascii=False) + '\n')这段预处理代码做了三件事:格式校验、长度过滤、截断。实际项目中,数据清洗能砍掉 10%~20% 的低质量样本,对最终 ROUGE 分数的提升比调参还明显。
2.2 为什么选 Transformer 而不是 LSTM
这是毕设里绕不开的一个选型问题。LSTM 做 seq2seq 摘要不是不行,但有两个硬伤:一是长距离依赖建模能力有限,原文超过 200 个 token 之后,前面的信息在解码时基本丢光了;二是无法并行计算,训练速度慢,本科毕设的计算资源通常有限,等不起。
Transformer 的自注意力机制直接把任意两个位置之间的路径长度缩短为 1,长文本建模能力有质的提升。而且训练时可以并行处理整个序列,同样数据量下训练时间能缩短一半以上。代价是显存占用更高,但对 512 token 以内的输入,单张消费级显卡完全够用。
具体到模型结构,我建议采用标准的 encoder-decoder 架构:编码器 6 层、解码器 6 层、注意力头数 8、隐藏维度 512。这个配置参数量大约 60M,在单卡上训练 10 万条数据大概需要 6~8 小时,是本科毕设比较合理的规模。
2.3 预训练模型微调 vs 从零训练
如果实验室有预训练模型权重可用,微调是首选。预训练模型已经在海量语料上学到了语法和语义知识,微调只需要让模型适应摘要任务的输出格式。学习率设 2e-5 到 5e-5,训练 3~5 个 epoch 就能收敛。
从零训练的话,数据量至少要到百万级别才能看到像样的效果,本科毕设的数据集通常达不到这个规模。所以我的建议很明确:优先用预训练权重初始化,把精力花在数据处理和推理策略上。
2.4 评估指标的选择
自动评估用 ROUGE 系列指标,这是文本摘要领域的通用标准。ROUGE-1 看 unigram 重叠,ROUGE-2 看 bigram 重叠,ROUGE-L 看最长公共子序列。三个指标各有侧重,答辩时通常报 ROUGE-1 和 ROUGE-L 的 F1 值。
人工评估也不能省。找 3~5 个同学,从流畅度、信息完整性、事实一致性三个维度打分,每个维度 1~5 分。自动指标高但人工评分低的情况很常见——模型可能复述了原文很多词,但句子根本读不通。
3. 模型设计与训练策略:编码器-解码器架构的落地细节
3.1 编码器与解码器的模块拆解
编码器由 N 个相同的层堆叠而成,每层包含两个子层:多头自注意力机制和前馈神经网络。每个子层后面接残差连接和层归一化。自注意力让每个 token 都能看到序列中所有其他 token,前馈网络则对每个位置独立做非线性变换。
解码器比编码器多了一个交叉注意力子层,用来关注编码器的输出。解码器的自注意力需要加掩码,防止当前位置看到未来的 token——这个细节如果漏掉,训练时 loss 会降得很快,但推理时生成的摘要完全是乱序的,属于典型的“训练看着好、推理全崩”的玄学问题。
位置编码用正弦余弦函数,不参与训练。这里有个血泪经验:位置编码的维度必须和词嵌入维度一致,否则相加时会报维度不匹配的错。我见过有同学在这里卡了一整天。
import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=512, dropout=0.1): super().__init__() self.dropout = nn.Dropout(dropout) # 预计算位置编码矩阵,避免每次前向传播重复计算 pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len).unsqueeze(1).float() div_term = torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) # shape: (1, max_len, d_model) self.register_buffer('pe', pe) def forward(self, x): # x shape: (batch_size, seq_len, d_model) x = x + self.pe[:, :x.size(1), :] return self.dropout(x)d_model是词嵌入维度,max_len是支持的最大序列长度。register_buffer把位置编码注册为缓冲区,不参与梯度更新但会随模型保存和加载。这个实现比在每个 batch 里现算要快不少。
3.2 训练配置与超参数设置
优化器用 Adam,初始学习率 2e-5,配合 warmup 策略——前 4000 步学习率线性增长,之后按余弦退火衰减。Batch size 设为 32,如果显存不够就降到 16,同时把梯度累积步数设为 2,等效 batch size 不变。
损失函数用交叉熵,但要注意 padding 位置不计算损失。做法是在计算 loss 时传入ignore_index参数,把 padding token 的 id 排除掉。这个细节不做的话,模型会花大量精力去学“怎么生成 padding”,纯属浪费容量。
import torch.nn.functional as F def compute_loss(logits, labels, pad_token_id=0): """ logits: (batch_size, tgt_len, vocab_size) labels: (batch_size, tgt_len) """ # 将 logits 展平后计算交叉熵,忽略 padding 位置 loss = F.cross_entropy( logits.view(-1, logits.size(-1)), labels.view(-1), ignore_index=pad_token_id, label_smoothing=0.1 # 标签平滑,缓解过拟合 ) return losslabel_smoothing=0.1是防止模型对训练数据过度自信,能提升泛化能力。这个值不要设太大,超过 0.2 会导致模型学不到东西。
3.3 推理阶段的束搜索策略
训练时用 teacher forcing,推理时如果还用贪心解码(每步选概率最大的词),生成的摘要容易重复、呆板。束搜索(beam search)维护 k 条候选路径,每步扩展后保留概率最高的 k 条,最终选整体概率最高的输出。
beam size 一般设 3~5。太小效果提升不明显,太大生成结果趋同且推理速度线性下降。另外要加长度惩罚,否则模型倾向于生成很短的摘要——因为短序列的累积概率天然更高。
def beam_search(model, src, beam_size=4, max_len=100, length_penalty=0.6): """ 简化版束搜索实现 length_penalty: 长度惩罚系数,越大越鼓励生成长摘要 """ model.eval() with torch.no_grad(): encoder_output = model.encode(src) # 初始化:每条候选序列从起始符开始 beams = [([model.bos_id], 0.0)] for _ in range(max_len): candidates = [] for seq, score in beams: if seq[-1] == model.eos_id: candidates.append((seq, score)) continue logits = model.decode(encoder_output, torch.tensor([seq])) log_probs = F.log_softmax(logits[:, -1, :], dim=-1) topk_probs, topk_ids = log_probs.topk(beam_size) for i in range(beam_size): new_seq = seq + [topk_ids[0][i].item()] new_score = score + topk_probs[0][i].item() candidates.append((new_seq, new_score)) # 按长度归一化后的分数排序,保留 top-k beams = sorted( candidates, key=lambda x: x[1] / (len(x[0]) ** length_penalty), reverse=True )[:beam_size] if all(s[-1] == model.eos_id for s, _ in beams): break return beams[0][0]length_penalty取 0.6~1.0 之间比较合适。等于 1.0 时就是纯长度归一化,小于 1.0 则对长序列更友好。这个参数对最终 ROUGE 分数的影响有时候比调学习率还大,值得多试几组。
4. 数据流水线与特征工程:从原始语料到可训练样本
4.1 中文分词与词表构建
中文和英文不同,词与词之间没有空格。做摘要任务时,分词粒度直接影响模型效果。我一般用 jieba 做分词,然后统计词频,保留出现次数最多的前 30000 个词作为词表,其余用<UNK>替代。
词表大小是个权衡:太小则<UNK>太多,信息丢失严重;太大则嵌入矩阵参数量暴涨,训练变慢。30000 是中文摘要任务里比较常用的取值。另外要预留四个特殊 token:<PAD>、<UNK>、<BOS>、<EOS>,分别用于填充、未知词、序列起始和序列结束。
import jieba from collections import Counter def build_vocab(corpus_path, vocab_size=30000, min_freq=2): """ 从语料中构建词表 corpus_path: 每行一条文本 vocab_size: 词表最大容量 min_freq: 最低词频阈值 """ counter = Counter() with open(corpus_path, 'r', encoding='utf-8') as f: for line in f: tokens = jieba.lcut(line.strip()) counter.update(tokens) # 特殊 token 优先占位 special_tokens = ['<PAD>', '<UNK>', '<BOS>', '<EOS>'] vocab = {tok: idx for idx, tok in enumerate(special_tokens)} # 按词频从高到低填充 for word, freq in counter.most_common(vocab_size - len(special_tokens)): if freq < min_freq: break vocab[word] = len(vocab) return vocabmin_freq=2意味着只出现一次的词直接归入<UNK>。这些词大概率是噪声或者拼写错误,保留它们只会增加模型负担。
4.2 数据增强与样本平衡
摘要数据集常见的问题是长尾分布——大部分样本的原文在 200~400 字之间,但少数样本超过 1000 字。直接训练的话,模型在长文本上的表现会明显偏差。
我通常的做法是:对超长样本做滑窗切分,切成多个 512 token 的片段,每个片段都对应原摘要。这样既保留了长文本的信息,又不会超出模型的最大输入长度。反过来,对过短样本可以做回译增强——把摘要翻译成英文再翻回中文,得到语义相同但表述不同的新样本。不过回译需要额外的翻译模型,本科毕设如果资源有限,可以跳过这一步。
4.3 DataLoader 的工程实现
自定义 Dataset 类时,最关键的是动态 padding——每个 batch 内按最长序列补齐,而不是全局统一长度。这样能节省大量显存,尤其是 batch 内长度差异大的时候。
from torch.utils.data import Dataset, DataLoader import torch class SummaryDataset(Dataset): def __init__(self, data_path, vocab, max_src_len=512, max_tgt_len=150): self.samples = [] self.vocab = vocab self.max_src_len = max_src_len self.max_tgt_len = max_tgt_len with open(data_path, 'r', encoding='utf-8') as f: for line in f: obj = json.loads(line) self.samples.append(obj) def __len__(self): return len(self.samples) def __getitem__(self, idx): obj = self.samples[idx] src_ids = self._encode(obj['source'], self.max_src_len) tgt_ids = self._encode(obj['target'], self.max_tgt_len) return torch.tensor(src_ids), torch.tensor(tgt_ids) def _encode(self, text, max_len): tokens = jieba.lcut(text)[:max_len - 2] ids = [self.vocab['<BOS>']] ids += [self.vocab.get(t, self.vocab['<UNK>']) for t in tokens] ids.append(self.vocab['<EOS>']) return ids def collate_fn(batch): """动态 padding:按 batch 内最长序列补齐""" srcs, tgts = zip(*batch) src_padded = torch.nn.utils.rnn.pad_sequence( srcs, batch_first=True, padding_value=0 ) tgt_padded = torch.nn.utils.rnn.pad_sequence( tgts, batch_first=True, padding_value=0 ) return src_padded, tgt_paddedcollate_fn里用pad_sequence自动完成动态 padding,比手动写循环简洁得多。注意padding_value要设成<PAD>对应的 id,通常是 0。
5. 避坑与常见问题排查:训练和推理阶段最容易翻车的五个点
5.1 损失降到很低但生成结果全是重复词
现象:训练 loss 降到 0.5 以下,但推理时模型反复输出同一个词或同一句话。
原因:最常见的原因是解码策略有问题。如果用的是贪心解码,模型一旦在某一步选了高频词,后续就会陷入循环。另一个可能是训练时没有加标签平滑,模型对训练分布过拟合。
解决:换成束搜索,加长度惩罚;检查训练 loss 是否降得过低(低于 0.3 就要警惕过拟合);在推理时加 repetition penalty,对已经生成过的词降低其概率。
5.2 ROUGE 分数异常低,但人工看摘要质量还行
现象:模型生成的摘要读起来通顺、信息也全,但 ROUGE-1 只有 20 出头。
原因:ROUGE 基于 n-gram 重叠,如果模型用了同义词替换或者句式重组,重叠率自然低。另外,如果参考摘要本身质量参差,评估结果也不可靠。
解决:检查测试集的参考摘要是否规范;可以补充 BERTScore 作为语义层面的评估指标;答辩时用人工评估结果辅助说明。
5.3 训练到一半 loss 突然变成 NaN
现象:前几个 epoch 正常,突然某个 batch 之后 loss 变成 NaN,模型参数全部失效。
原因:梯度爆炸。Transformer 虽然比 RNN 稳定,但学习率设太大或者 warmup 步数不够时仍会出现。
解决:加梯度裁剪,torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0);检查学习率是否超过 5e-5;warmup 步数不要低于 2000。
5.4 显存不够导致 batch size 只能设很小
现象:8G 显存跑 512 长度的输入,batch size 只能设到 4,训练速度极慢。
原因:Transformer 的注意力矩阵占用显存与序列长度的平方成正比,512 token 时注意力矩阵已经不小了。
解决:用混合精度训练(AMP),显存占用能降 30%~40%;开启梯度检查点(gradient checkpointing),用计算时间换显存;或者把 max_src_len 从 512 降到 384,效果损失通常不超过 1 个 ROUGE 点。
5.5 验证集 loss 持续下降但 ROUGE 不再提升
现象:训练了 10 个 epoch,验证集 loss 还在降,但 ROUGE-1 从第 5 个 epoch 之后就不动了。
原因:模型在优化 token 级别的似然,但 ROUGE 衡量的是 n-gram 重叠,两者不完全一致。继续训练只会让模型更“自信”,不一定生成更好的摘要。
解决:以 ROUGE 为早停依据,而不是验证集 loss;通常 5~8 个 epoch 就能到最优,再训下去收益极小。我一般会每训练一个 epoch 就跑一次验证集生成,记录 ROUGE 变化曲线,选最高点保存模型。
6. 验收与答辩:怎么证明你的摘要模型真的能用
验收这件事,很多同学到了答辩前一周才开始想,结果发现除了 loss 曲线什么都拿不出来。我的习惯是从项目第一天就把验收标准定好,训练过程中持续记录,到最后直接整理成答辩材料。
自动评估的完整流程。在测试集上跑一遍生成,用 ROUGE 脚本计算 ROUGE-1、ROUGE-2、ROUGE-L 的 Precision、Recall、F1。通常报告 F1 值。一个合格的本科毕设,ROUGE-1 F1 应该在 35~45 之间,ROUGE-L F1 在 30~40 之间。如果低于这个区间,先检查数据预处理和推理策略,而不是急着换模型。
人工评估的设计。找 5 个同学,随机抽 50 条测试样本,每条给出原文、参考摘要、模型摘要。评估者从三个维度打分:流畅度(句子是否通顺)、信息完整性(关键信息是否保留)、事实一致性(是否有与原文矛盾的内容)。每个维度 1~5 分,最后算平均分和标准差。标准差大的话说明模型表现不稳定,需要分析是哪些类型的样本出了问题。
答辩演示的技巧。现场演示不要只放 ROUGE 表格,评委看不出直观差异。准备三组对比:模型摘要 vs 参考摘要 vs 原文截断。让评委直观感受模型确实在“理解后重新表达”,而不是简单复制。另外准备一两个失败案例,主动分析原因,比只展示成功案例更能体现你对方案的理解深度。
常见答辩问题准备。评委大概率会问:为什么选 Transformer 不选 BERT?束搜索的 beam size 怎么定的?ROUGE 分数和人工评估是否一致?这些问题在方案设计阶段就应该有答案,不要等到答辩现场现编。
| 验收项 | 合格标准 | 检查方式 |
|---|---|---|
| ROUGE-1 F1 | ≥ 35 | 测试集自动评估脚本 |
| ROUGE-L F1 | ≥ 30 | 同上 |
| 人工流畅度均分 | ≥ 3.5 / 5 | 5 人独立打分取平均 |
| 事实一致性均分 | ≥ 3.5 / 5 | 同上 |
| 推理速度 | ≤ 2 秒 / 条 | 单条 512 token 输入计时 |
| 失败案例分析 | 至少 3 类 | 人工归类总结 |
从那以后我每次带毕设,都要求学生在开题阶段就把这张验收表填好,训练过程中每周更新一次数据。到了答辩前,材料是现成的,不用熬夜补实验。希望这套方案能帮到你,少走一些我当年踩过的弯路。
本文还有配套的精品资源,点击获取