1. 选题价值与前置条件:先把“隐形成本”算清楚
1.1 这个题目在毕业设计体系里的定位
先说句实话:每年深度学习方向的毕业设计,标题里带“新闻摘要”的不少,但大部分做出来其实就是调用预训练模型接口,自己写个界面就交差了。真正从数据清洗、词表构建、模型训练到评价指标完整走一遍的项目,反而成了稀缺品。如果看到“Encoder-Decoder框架模型”这种描述,意味着评委大概率要问编码器怎么实现、解码器怎么生成、注意力机制加在哪一层。它能体现你对序列建模、概率生成、梯度训练这些深度学习核心概念的理解,而不是单纯把框架API堆在一起。
从答辩角度讲,新闻摘要生成是一个“看得见、摸得着、讲得清”的任务。输入一篇新闻长文本,输出一条精简摘要,任何人不需要背景知识就能理解你的项目在做什么。比起那些“基于某某改进算法的某某分类系统”,摘要生成既有直观的业务价值,又有丰富的技术细节可以深挖。做得好,是一个在三五分钟内就能让评委进入状态的项目。
但也要清醒:这个题目的隐形成本不低。所谓“隐形成本”,指的是那些不在选题报告里、但在实际开发中一定会消耗大量时间的环节。比如语料的清洗对齐、长文本训练时的显存控制、摘要评价指标的理解,以及各种“看起来训练正常但结果全是乱码”的玄学问题。论文里写“我们构建了基于注意力机制的序列到序列模型”只需要一行,但这一行背后可能要调两周的参数。所以第一步不是打开编辑器写模型,而是先把数据和硬件盘子算清楚。
1.2 数据、硬件与时间预算
中文新闻摘要训练数据,标准做法是使用公开的中文新闻语料,通常包含“正文—标题”或“正文—摘要”的成对样本。以常见的中文摘要基准数据来看,一般会提供数十万条新闻标题对,每条由新闻正文拼上手工摘要构成。没有公开语料时也可以自己抓取新闻站点的正文与标题,清洗后作为训练对。这里有个细节:标题并不完全等于摘要,但标题通常是对正文核心信息的凝练,搭配少量正文抽取句就能形成质量可接受的训练目标。
数据清洗阶段要以“样本配对合理、长度分布可控”为原则做几件事:
- 去除HTML标签、乱码、超长链接和广告残留字符。
- 过滤正文过短(少于50字)或摘要过短(少于5字)的样本。
- 按正文长度做分桶统计,控制最大长度在512个token以内,减少显存压力。
- 去除重复样本,尤其是新闻站点常见的“转发型”内容,避免模型把背诵当能力。
硬件上,训练一个两层的Bi-GRU+Attention模型,单张消费级显卡绰绰有余,16GB显存已经能宽松处理batch size 64的短文本训练。如果显卡只有6GB,就把batch size降到32、词表控制在3万以内,一样能跑完。和预训练模型动辄几十小时的微调相比,自建编码器解码器在算力上反而是省钱省时的。
时间分配我个人建议按“数据:代码:调参:写论文 = 2:3:3:2”的比例来规划。数据清洗和代码骨架尽量在两周内完成,因为后面的大部分时间都会消耗在“训练看起来正常、生成结果却很差”的排查里,这部分不在计划里预留时间,很容易拖到答辩前才通宵补救。
2. 从信息压缩到条件生成:新闻摘要任务的形式化建模思路
2.1 摘要本质上是受约束的二次生成
站在建模角度,新闻摘要要做的事是:给定一个源序列 (x = (x_1, x_2, ..., x_T)),长度为几百个词;输出一个目标序列 (y = (y_1, y_2, ..., y_S)),长度通常只有几十个词。(S) 远小于 (T),所以这本质上是一个“有损压缩”问题,但和传统压缩算法完全不同之处在于:它压缩的不是字节,而是语义。
Encoder-Decoder结构的核心假设是,输入信息可以被编码成一个上下文表示,再由解码器基于这个表示逐词生成输出。写成概率形式就是:
[ P(y|x) = \prod_{t=1}^{S} P(y_t | y_{<t}, c) ]
其中 (c) 是编码器生成的整体上下文向量。这个公式虽然简单,但它把“理解原文”和“组织语言”两个任务拆开了:编码器负责理解,解码器负责生成。这种拆分非常贴合摘要场景——新闻里大量事实信息需要在编码阶段被压缩提炼,输出阶段再把提炼结果转写成通顺的句子。
可以这样类比:编码器像是一位速记员,把一篇冗长的会议记录压成几个关键要点;解码器则像一位发言人,看到这些要点后用完整句子重新组织出一条口播稿。如果速记员记漏了背景信息,无论发言人语言能力多强,都会跑偏;如果记录完整但发言人表达能力差,输出同样不可读。所以训练摘要模型时,编码器和解码器必须同时优化,这也是联合训练的直观原因。
2.2 抽取式与生成式两条路线
摘要生成历史上存在两条路线,理解它们的区别对答辩很重要。
抽取式摘要从原文中选出若干重要句子,按顺序拼接成摘要。优点是忠实度高、不易产生事实错误;缺点是语句不连贯,经常出现“摘要是由好几个句子硬拼出来的”的生硬感。经典的TextRank算法就是这个思路的典型代表,它在没有深度学习的情况下也能跑出可用的基线结果。
生成式摘要则允许生成全新词汇组合,流畅度更高,更接近人写的摘要。缺陷是容易出现生成信息与原文事实不符的情况,业内常把这称为“幻觉”。做生成式项目,评委大概率会追问:“模型生成的摘要里出现了一个数字,而这个数字在原文中不存在,怎么解决?”
抽样式的选题在答辩时的好处是,可以坦率地回答:正因为抽取式有拼接僵硬、缺乏概括能力的问题,所以选择生成式路线来做端到端建模。这样既说明了你了解传统方案,也表明你的选择是经过比较的,而不是随意拍脑袋。
2.3 新闻文本的特性:倒金字塔与高信息密度
新闻体例和其他文本最大的不同在于“倒金字塔结构”:最重要的信息(时间、地点、人物、事件、结果)通常放在第一段,背景和细节依次后移。这给摘要任务带来了天然红利——训练时模型很容易学到“前文比后文更重要”的隐式先验。但也是陷阱,新闻里的专有名词、数字、百分比、地名出现频率极高,而这些恰恰是模型最容易出错的部分。
举个例子,原文是“某地发生5.2级地震,震源深度8公里”,如果模型生成“某地发生5级地震,震源深度约8公里”,虽然语义接近,但在地震报道中,“5.2级”和“5级”是完全不同的信息。自动评价指标ROUGE会对这种偏差扣分,人工评价更会觉得“不严谨”。处理这类问题的常用手段是引入复制机制,让解码器在生成专有名词和数字时直接从原文中拷贝对应片段,这是后面章节要展开的优化方向。
3. 基于序列到序列的网络骨架:编码器、解码器与注意力接入点
3.1 数据清洗与词汇表构建
数据准备好之后,第一步是把文本切分成模型能处理的最小单元。中文不像英文有天然空格分词,需要借助分词工具。这里不推荐在RNN类模型上直接使用字级别,字级别会让序列过长,且丢失词汇语义;也不建议一开始就把词表拉得太高,3万到5万词通常够用。
一个实用的词表构建流程:
import jieba def build_vocab(sentences, vocab_size=30000): counter = {} for line in sentences: for word in jieba.cut(line.strip()): counter[word] = counter.get(word, 0) + 1 # 过滤低频词 filtered = {w: c for w, c in counter.items() if c >= 2} # 按频率降序 sorted_words = sorted(filtered.items(), key=lambda x: -x[1]) words = [w for w, _ in sorted_words[:vocab_size]] voc = {"<pad>": 0, "<unk>": 1, "<bos>": 2, "<eos>": 3} for idx, w in enumerate(words): voc[w] = idx + 4 return voc这里有两个细节容易被忽略。第一,词频过滤阈值调到2以上,否则词表会被大量只出现一次的人名地名污染,模型无法学到有效表示。第二,特殊符号里<pad>必须排在0,因为RNN类模型在padding位置要掩码,从0开始能简化后续处理。
对每一条样本,正文做截断、摘要做截断,并在摘要前后加上起始符和结束符。解码器训练时的目标是<bos> 摘要内容 <eos>,没有结束符,模型就不知道什么时候该停下来。
3.2 编码器实现:为什么我推荐双向GRU
教科书里编码器示例通常是LSTM,但实际做项目时我推荐GRU,尤其是双向GRU。GRU参数比LSTM少,在小数据集上更不容易过拟合,训练速度也快一些;而双向结构能让每个位置同时看到上文和下文,对“哪句话才是重点”的判断更准确。
代码骨架大致是这样:
import torch import torch.nn as nn class Encoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, num_layers=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_size, padding_idx=0) self.gru = nn.GRU(embed_size, hidden_size, num_layers=num_layers, batch_first=True, bidirectional=True, dropout=0.5) def forward(self, x, lens): emb = self.embedding(x) packed = nn.utils.rnn.pack_padded_sequence(emb, lens.cpu(), batch_first=True, enforce_sorted=False) outputs, hidden = self.gru(packed) outputs, _ = nn.utils.rnn.pad_packed_sequence(outputs, batch_first=True) return outputs, hidden注意pack_padded_sequence的使用:新闻文本长度差异很大,不按长度打包直接训练,GPU会按最长样本做大量无效计算。这是一个经常被忽略的优化点,做好了训练效率能提升30%以上。
3.3 解码器与注意力机制的接入
朴素Decoder把编码器最后一个隐藏状态当成唯一上下文,训练短句时表现尚可,但新闻正文动辄几百字,尾部信息很容易被长程建模过程中的信息衰减覆盖掉。解决办法是每一时间步重新计算一个“相关的”上下文向量,这就是注意力机制。
注意力机制可以理解为解码器在生成每个词时,先对编码器的所有隐藏状态做一次加权求和:
[ c_t = \sum_{i} \alpha_{t,i} h_i ]
权重 (\alpha_{t,i}) 由解码器上一状态和编码器第 (i) 个位置的隐藏状态共同计算。以加性注意力为例:
class Attention(nn.Module): def __init__(self, hidden_size): super().__init__() self.attn = nn.Linear(hidden_size * 2, hidden_size) self.v = nn.Linear(hidden_size, 1, bias=False) def forward(self, decoder_hidden, encoder_outputs, mask): # decoder_hidden: (batch, hidden_size) batch_size, src_len, _ = encoder_outputs.size() decoder_expanded = decoder_hidden.unsqueeze(1).expand(batch_size, src_len, -1) energy = torch.tanh(self.attn(torch.cat([decoder_expanded, encoder_outputs], dim=-1))) scores = self.v(energy).squeeze(-1) # (batch, src_len) scores = scores.masked_fill(mask == 0, -1e9) weights = torch.softmax(scores, dim=-1) context = torch.bmm(weights.unsqueeze(1), encoder_outputs).squeeze(1) return context, weights这里masked_fill是把padding位置置为负无穷,避免模型把注意力放到无意义的补位符上。这个掩码如果不做,生成的摘要开篇经常出现“的了么啊”这类无意义词,是特别常见的踩坑点。
有了注意力,Decoder可以“按需取用”原文任意位置的信息。这相当于给发言人一份可以随时翻阅的会议记录,而不是只靠开场前硬记的几句话。对一个追求信息保真的摘要任务来说,注意力不是锦上添花,而是刚需。
3.4 为什么不直接上Transformer
我知道很多人会问:都什么时候了,怎么还在做GRU+Attention?毕业设计选这个组合,原因很实际。Transformer的显存占用、训练技巧复杂度都更高,作为本科或硕士项目,很容易陷在“跑不动、调不好、讲不清”的泥潭里。而GRU+Attention是理解所有现代生成模型的极好地基:注意力、Teacher Forcing、Beam Search这些概念在Transformer里同样需要。先把RNN时代这套链路吃透,再在论文里加一段“改用预训练Transformer编码器”的展望,反而显得思路非常清晰。
4. 训练阶段最容易忽视的五个细节
4.1 Teacher Forcing的比例与过度依赖
训练解码器时,最常用的方法是Teacher Forcing:每一步的输入都使用真实的上一词,而不是模型自己生成的词。这相当于“做题时直接给你标准答案的第一步”,好处是收敛快、训练稳定;坏处是推理阶段没有标准答案可用,模型必须用自己生成的低质量输出继续往后推,训练与推理不一致。
解决这个问题的标准做法是计划采样:训练前期全程Teacher Forcing,后期按一定概率(比如0.3)替换成模型自己预测的词。这样模型逐步学会修正自身错误,不至于到推理时崩盘。要注意的是概率不能调得太大,否则训练过程会非常不稳定,loss曲线上下乱跳。
4.2 重复词问题:摘要最顽固的怪病
训练一两轮后生成结果最常见的病态是“土豆土豆土豆土豆”式的重复。原因有两个:一是训练语料里摘要普遍偏短,模型没有学到何时收手;二是注意力在从头生成时出现退化,某几个位置的权重越来越高,导致反复输出同一个词。
处理手段有几个层次。解码端最常用的是在Beam Search时加入重复惩罚,对已经输出过的n-gram重复做扣分。训练端则可以在损失函数中增加覆盖损失项,惩罚多个时间步重复访问相同原文位置。把这些技巧写进论文,比单纯展示“我们用了注意力”更有说服力,因为说明你确实碰到了问题并且有解决方案。
4.3 未登录词与复制机制
新闻里的机构名、人名、新兴网络词,几乎注定有一部分不在词表里,会用<unk>代替。模型生成结果里一旦出现<unk>,这条摘要基本就废了。解决方向有两个:
- 把词表做大,但词表过大会拖慢训练,效果提升有限。
- 采用指针生成网络:在解码器生成每个词时,先计算一个“生成概率”,若概率低,则直接从原文拷贝一个token。这种机制能让摘要完整保留“某地5.2级地震”这类精确表达。
作为毕业设计,不必把复制机制完整实现为攻关重点,但在论文中把它作为“未来工作”或“可选的改进点”提出来,会让评委觉得你对该任务的痛点有清楚认知。
4.4 梯度裁剪与学习率策略
RNN训练中最常见的崩坏现场是loss突然变成NaN,或者是训练两三个小时后loss不降反升。排查时第一件要做的事就是梯度裁剪,将梯度范数限制在5左右。不要觉得这个小技巧不值得写进论文,实际上它经常是项目从“失败”到“成功”的分水岭。学习率上建议使用倒计时衰减,前几千步用一个较高的预热学习率,之后逐步降低。Adam优化器虽然对学习率不敏感,但在生成任务上,学习率设在0.001或0.0005是一个比较安全的起点。
4.5 日志与随机种子的可复现性
这个细节论文里不会提,但实验过程中非常重要。开始训练前设置随机种子,记录超参数配置到config.json,每次实验固定一个目录保存模型权重和生成样例。否则过了两天,你发现模型效果变好,但完全想不起来改了什么参数,复现时只能靠猜。做实验和写代码一样,版本管理做不好,返工成本极高。
5. 评价指标与实验设计:让答辩变成一个完整故事
5.1 ROUGE评价指标到底在算什么
ROUGE是摘要任务最重要的自动评价指标。核心思想是把模型生成的摘要和参考摘要做“片段重合度”比较。
- ROUGE-1:按单词或词语计算重合程度,主要衡量内容保留是否充分。
- ROUGE-2:按二元词组计算重合程度,更关注流畅度和词序。
- ROUGE-L:用最长公共子序列计算,兼顾内容与顺序。
举例来说,参考摘要“美联储宣布加息25个基点”,模型生成“美联储宣布加息”,ROUGE-1重合度较高,ROUGE-2较低,因为没有形成完整二元词组。自动指标的局限性在于它不理解语义,参考摘要里的“下调”和生成里的“降低”语义相同,但ROUGE一点重合都不给。所以论文呈现时建议说“ROUGE指标偏高说明生成内容在词汇层与参考摘要接近”,而不是直接说“ROUGE代表摘要质量”。
中文场景下评估细节值得一提:标准的ROUGE实现通常以空格分词为基准,中文需要先分词再评估。使用字级别的ROUGE也是一种常见做法,在摘要较短时字级别会比词级别更稳。
5.2 三组对比实验的设计思路
毕业设计如果只有“训练了一个模型然后给出一些案例”,评价上会很单薄。至少设计三组实验:
| 模型 | 结构描述 | ROUGE-1 | ROUGE-2 | ROUGE-L |
|---|---|---|---|---|
| Baseline A | 抽取式TextRank | 30.2 | 13.5 | 25.8 |
| Baseline B | 单向单层GRU + 朴素Decoder | 31.4 | 14.2 | 26.5 |
| 本项目模型 | 双向GRU + Attention + Beam Search | 36.7 | 18.4 | 30.1 |
这张表讲清楚了一个非常漂亮的故事:抽取式方法合并文本但句子生硬;朴素序列到序列模型在短句上有一定效果但长文上信息遗漏严重;加入双向编码和注意力后,模型能够根据注意力权重回溯原文重点,指标获得可观提升。答辩时围绕这张表展开,比堆砌十个指标更有效。
5.3 案例分析与失败模式的呈现
实验部分除了指标,还要放几个真实案例。建议放两组:一组是模型表现好的,摘要忠实且通顺;另一组是模型表现差的,比如生成了和原文相悖的时间或数字。主动在论文里展示失败案例,会让整个工作显得更诚实也更有深度。然后针对失败案例做简短分析,例如“该样例中出现日期错误,原因是训练语料中事件日期多出现在正文中后段,解码注意力在长程信息提取上仍然不够稳定,后续可通过复制机制缓解。”
顺便提一个答辩技巧:把注意力权重可视化出来,选取一篇新闻,展示解码过程中模型在第几个词上关注原文哪句话。这个可视化图信息量大,而且立刻能让评委理解“注意力机制到底学到了什么”。它比任何文字描述都更有说服力。
6. 完成核心模型后的扩展方向与个人体会
6.1 我推荐在有限算力下尝试的改进
如果核心模型已经跑通且时间还够,不要一上来就试图换成大规模预训练模型。先做两个低成本的改进效果更明显:第一,把Beam Search换成带长度惩罚的版本,生成摘要长度会更合理;第二。在训练时对摘要长度做加权控制。把常规摘要目标设定为“不超过原文三分之一”,模型不易生成过长的冗余摘要。
另一个真实有效的方向是把预训练语言模型当作编码器接入Encoder-Decoder。不少开源中文预训练模型可以提供高质向量,只需要冻结底层参数,只训练注意力层和解码器。这个做法能在不巨大增加算力的情况下显著提升ROUGE分数,论文里也很容易写成“我们探索了预训练表示与生成式摘要任务的结合”。
6.2 实验记录与时间管理的实操心得
做这个项目最大的感受是:深度学习训练是“非线性”的。前一晚模型效果还很差,早上起来发现验证集指标上涨了一截,这是常见现象。反过来,也有连续三四天没有指标变化,怎么调都纹丝不动的情况。我的建议是设定一个固定间隔,比如每五个epoch保存一次checkpoint,并且把每次生成的样例文本存下来。人工观察生成样例往往比盯损失曲线更能发现问题。损失在降但摘要全是重复词,这种情况光看loss完全看不出来,必须打开生成文件看输出结果。
时间管理上,最怕的不是调参,而是反复“从零开始”。数据预处理脚本、词表文件、模型代码、训练日志,全部都要编号归档。用一个简单的文件命名规则:v1_lr0.001_bs64_epoch15,同目录下放对应的配置文件。前期看似多花了一点时间,后期却会省下几个通宵。
6.3 最后想说的“真话”
新闻摘要生成这个题目,技术门槛不是最高的,但它把深度学习中最经典的问题集齐了:序列建模、概率生成、信息压缩、内容忠实性和自动评价。把这些链路完整走下来,比只跑通一个现成的模型训练脚本要值钱得多。答辩时最重要的不是模型得分高到惊人,而是你能在一系列决策点上说出“我为什么这么做、遇到了什么问题、如何权衡”。评委想看到的,就是这样一条清晰的思考路径。
如果未来还想继续深入,可以把方向延伸到多文档摘要、可控摘要(指定摘要风格或长度),以及融合知识图谱的忠实性约束。这个项目的结构决定了它的扩展面很广,随时能长出新东西。有一点可以确定:把Encoder-Decoder这套基本功练扎实了,后面理解更复杂的生成模型会轻松很多。