恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
深度学习新闻摘要生成实战:Encoder-Decoder与注意力机制全解析
首页
资讯中心
/
深度学习新闻摘要生成实战:Encoder-Decoder与注意力机制全解析
深度学习新闻摘要生成实战:Encoder-Decoder与注意力机制全解析
发布时间:2026/10/20 20:32:10
又是一年毕业设计季要说这几年哪个方向最热门“基于深度学习的新闻摘要生成”绝对排在前列。很多同学选这个题目第一反应是“听起来高级”但实际上这个题目的核心就落在Encoder-Decoder框架上也就是我们常说的Seq2Seq模型。把它吃透了整个毕设就完成了一大半。这篇文章我不打算讲那种“从入门到放弃”的泛泛而谈直接以我实际完成这个项目的过程为主线把选题思路、模型搭建、数据预处理、训练调参、评估指标到最终踩坑记录全部展开来说。项目基于PyTorch实现主打一个“能跑通、能复现、能答辩”。无论你是正在摩拳擦掌准备开题还是训练跑到一半发现loss不降这篇内容应该都能给你一些实际参考。1. 选题破局为什么新闻摘要值得做以及毕设的边界在哪里1.1 新闻摘要的任务本质与学术价值新闻摘要生成本质上是“长文本压缩”任务。给模型一篇动辄几千字的新闻报道让它输出一段一两句话的核心观点这就牵扯到两个核心问题理解原文讲了什么以及把理解到的信息重新组织成通顺的语言。从学术角度看这个任务比很多分类任务或回归任务要“高级”一点因为它是典型的序列到序列Sequence-to-Sequence问题。输入是变长的词序列输出也是变长的词序列。这逼迫你必须去接触和掌握Encoder-Decoder这个核心框架而不只是调个包、跑个接口。从毕设角度看这个题目的优势特别明显门槛适中不需要你发明新网络结构PyTorch和HuggingFace生态已经帮你把基础组件准备好了。延展性极强你做了基础的Seq2Seq之后可以很自然地往注意力机制、Transformer、预训练模型方向延伸。写论文的时候“创新点”比较好铺。效果可见训练完成之后你可以真实地输入一篇新闻看到模型输出的摘要。演示效果好答辩时不心虚。1.2 毕设范围控制一步到位还是循序渐进这里必须先泼一盆冷水不要把毕设的“野心”定得太高。很多人上来就想做基于BERT或者GPT的摘要生成觉得效果肯定好。但毕设的核心不是“效果最好”而是“研究过程完整、原理清楚、实验扎实”。我建议的边界线是这样的底线版本保证能毕业基础的Encoder-Decoder 注意力机制在小型新闻数据集上完成训练和测试ROUGE指标作为主要评估。进击版本拿优秀论文在底线版本基础上对比多组实验比如双向LSTM vs 单向LSTM是否使用注意力机制不同Beam Search宽度的影响等。冒险版本硬核挑战融入Transformer结构或者尝试用预训练模型做微调并和基线模型做对比分析。我当时就选择了“进击版本”在基础框架上做了一组消融实验既稳又有话可写。这里特别建议和导师确认一下对论文创新点的预期再决定投入多少精力去升级模型。2. 数据生命线新闻数据的获取、清洗与预处理全流程2.1 数据集选型自己爬还是用现成的新闻摘要数据集最省事的选择是几种公开中文数据集比如LCSTS中文短文本摘要数据集里面是微博风格的短文和摘要规模大概在200万条左右。还有“大规模中文自然语言处理数据集”里汇总的多个子集以及一些英文经典数据集如CNN/DailyMail适合做方法验证。如果导师要求“必须自己采集数据”来体现工作量那可以选择爬取新闻网站的“标题-正文”对。但说实话这一步工作量巨大爬虫、清洗、去重、格式统一哪个都不能少而且容易陷入数据版权争议。我个人的建议是优先使用公开数据集然后补充一小部分自己采集的数据做测试展示。这样既保证了训练可行性又体现了项目真实性。2.2 数据清洗别让脏数据毁掉你的训练拿到原始数据后清洗这一步直接决定模型上限。新闻摘要数据的常见问题包括标题和正文中存在大量空格、换行、特殊符号。网页爬取数据中残留HTML标签。中文标点不统一半角全角混合。存在重复样本、超长样本、空样本。我的清洗流程大致分5步1. 统一转成UTF-8编码去除不可见字符 2. 全角标点转半角中文句子中的人名、数字等也做规范化 3. 用正则表达式剔除HTML标签和非法字符 4. 将连续的多个空白字符压缩为单个空格 5. 按“标题长度 5”和“正文长度在 20~500 字之间”过滤异常数据特别强调两点摘要长度过滤很重要。新闻摘要通常控制在15到30个字之间太短的摘要比如只有两三个字信息量不足太长的摘要又和正文区别不大。我最终把摘要长度上限设置为50个字超过的样本直接丢弃。中文分词需要做吗如果你用的是LSTM/GRU这类模型强烈建议做分词否则词表会非常大而且稀疏。我当时用的分词工具是结巴jieba加载了自定义的新闻领域词典把“新基建”“数字经济”这类词整体切分效果比默认模式好不少。如果直接用BERT这种带词表的模型分词这步就可以交给Tokenizer处理后面我会提到。2.3 构建词表与数据管道清洗完成后把正文和摘要都切成词序列然后统计词频构建词表。有两个核心参数需要自行决定词表大小和序列最大长度。词表我设置的是30000加上四个特殊符号pad填充、unk未登录词、bos起始符、eos结束符。序列长度方面正文最大长度设为200个词摘要最大长度设为50个词。超过截断不足补齐。这里有个经验数值词表30000基本能覆盖当前数据集99%以上的词汇。词表太小会导致大量unk出现模型学不到语义词表太大则会导致Embedding层参数量剧增训练变慢而且大量低频词根本学不到有用的表示。数据管道的标准做法是重写PyTorch的Dataset类和DataLoader在每个batch内部对序列进行动态padding这样比全数据集固定长度padding要节省很多显存和计算量。3. 模型设计Encoder-Decoder框架逐层拆解3.1 整体架构从宏观到微观新闻摘要生成最基础的模型就是Seq2Seq架构它由两部分组成Encoder编码器读入整篇新闻把信息编码成一系列隐状态向量。Decoder解码器根据编码器输出的隐状态逐词生成摘要。我构建的模型主体采用双向LSTM作为编码器单向LSTM配合注意力机制作为解码器。为什么选双向LSTM而不是单向理由很简单新闻的关键信息可能出现在句子的任何一个位置遇到后面才出现的重要实体和事件时单向编码会丢失前文信息。双向编码器能让每个时间步的隐状态同时包含左侧和右侧的上下文这对摘要生成来说非常关键。补充一个概念用生活化的类比来解释LSTM的作用假设你在读一篇长新闻LSTM相当于是带着“记事本”的阅读者每读一个词就更新记事本决定哪些信息该记住、哪些该忘掉读到结尾时记事本上的内容就是对全篇新闻理解的浓缩。而双向LSTM像是两个人同时从开头和结尾翻过来读在中间汇合信息覆盖更全面。3.2 Embedding层与参数形状先看代码我再逐步解释关键参数class Encoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, num_layers1, dropout0.3): super(Encoder, self).__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idx0) self.lstm nn.LSTM( input_sizeembed_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0 ) def forward(self, src, src_len): embedded self.embedding(src) packed nn.utils.rnn.pack_padded_sequence( embedded, src_len.cpu(), batch_firstTrue, enforce_sortedFalse ) packed_outputs, (hidden, cell) self.lstm(packed) outputs, _ nn.utils.rnn.pad_packed_sequence(packed_outputs, batch_firstTrue) return outputs, hidden, cell关键参数解释vocab_size30000词表大小对应Embedding矩阵的第一维。embed_size256词向量的维度把每个词映射成一个256维的稠密向量。hidden_size256LSTM隐状态的维度。bidirectionalTrue双向LSTM每个方向的隐状态维度都是256拼接后成为512维。自检一遍src的形状是(batch_size, seq_len)embedded的形状是(batch_size, seq_len, embed_size)LSTM输出的outputs形状是(batch_size, seq_len, hidden_size * 2)。因为用了pack_padded_sequence短样本不会计算多余的padding部分这在训练长新闻时能省不少时间。另外别忘了长度序列src_len要放在CPU上否则会报设备不匹配。3.3 注意力机制给摘要生成装上“指路明灯”纯Seq2Seq有一个严重问题无论新闻多长Encoder都把整篇内容压缩到最后一个时间步的隐状态里。这就好比让你看完一部两个小时的电影后只靠最后一帧画面去复述整部电影信息丢失太严重了。注意力机制的思路是解码器生成每个词的时候不只依赖最后那个状态而是主动回看编码器的所有隐状态挑选出与当前生成词最相关的信息。具体计算分三步用解码器当前隐状态和编码器的每一个隐状态做相似度计算得出注意力分数。对注意力分数做Softmax归一化得到权重分布。用权重对编码器隐状态加权求和得到一个上下文向量context vector。注意力分数的计算最常用的是加性注意力Additive Attention和点积注意力Dot-Product Attention我用了加性注意力原因是它在处理LSTM隐状态这种维度不匹配或需要非线性的情况时更稳定。class Attention(nn.Module): def __init__(self, hidden_size): super(Attention, self).__init__() self.W1 nn.Linear(hidden_size * 2, hidden_size) # Encoder输出维度 self.W2 nn.Linear(hidden_size, hidden_size) # Decoder隐状态维度 self.V nn.Linear(hidden_size, 1, biasFalse) def forward(self, encoder_outputs, decoder_hidden): # encoder_outputs: (batch, src_len, hidden*2) # decoder_hidden: (batch, hidden) attn_scores self.V(torch.tanh( self.W1(encoder_outputs) self.W2(decoder_hidden).unsqueeze(1) )).squeeze(2) attn_weights F.softmax(attn_scores, dim1) context torch.bmm(attn_weights.unsqueeze(1), encoder_outputs).squeeze(1) return context, attn_weights细心的读者可能发现了W2(decoder_hidden)输出的维度是(batch, hidden)而W1(encoder_outputs)输出的维度是(batch, src_len, hidden)两者相加之前先对decoder_hidden做了一次unsqueeze(1)变成(batch, 1, hidden)。这样广播相加后每个源位置都能获得一个分数。这里有一个经常被问到的细节decoder_hidden取的是哪一层的哪个方向在我们的设计里双向LSTM的最终隐状态把两个方向拼成了(num_layers, batch, hidden*2)但我并没有直接拿全部方向去解码而是把一个方向拆分出来作为解码器的初始状态。具体做法后面给出。3.4 Decoder与Beam Search生成策略Decoder部分的实现要点有两个一个是每一步如何用“前一时刻输出 当前上下文”生成当前词另一个是推断时用Beam Search搜索更优的生成序列。class Decoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, num_layers1, dropout0.3): super(Decoder, self).__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idx0) self.lstm nn.LSTM( input_sizeembed_size hidden_size * 2, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0 ) self.attention Attention(hidden_size) self.fc nn.Linear(hidden_size, vocab_size) self.dropout nn.Dropout(dropout) def forward(self, input_token, last_hidden, last_cell, encoder_outputs): # input_token: (batch, 1)就是上一步生成的词 embedded self.dropout(self.embedding(input_token)) context, attn_weights self.attention(encoder_outputs, last_hidden[-1]) lstm_input torch.cat((embedded, context.unsqueeze(1)), dim-1) output, (hidden, cell) self.lstm(lstm_input, (last_hidden, last_cell)) output self.fc(output.squeeze(1)) return output, hidden, cell, attn_weights这里最需要理解的是lstm_input的处理我们把当前词的Embedding和注意力上下文向量拼接起来再作为LSTM的输入。也就是说解码器在生成下一个词时既知道“上一次说了什么”也知道“现在应该关注原文的哪个部位”。训练阶段用的是Teacher Forcing策略不管上一步预测的结果对不对都把真实摘要词作为下一步的输入。这样做的好处是收敛快、训练稳定。如果完全不用Teacher Forcing模型在前期预测质量很差错误会像雪球一样越滚越大训练很难跑起来。推理阶段生成摘要我使用Beam Search宽度设为3意思就是每一步保留概率最高的3个候选序列。相比于每次都挑最大概率的单词即Greedy SearchBeam Search考虑到了“当前选这个词可能不是全局最优”的情况实际生成的摘要连贯性要好很多。4. 优化器、损失函数与训练流程设计4.1 参数初始化与优化器选择模型参数的初始化直接影响到训练能否正常启动。LSTM的隐状态矩阵如果初始化不当容易导致梯度消失或梯度爆炸最常见的做法是均匀分布初始化或正交初始化。我使用了PyTorch默认的初始化方式但额外对Embedding层的梯度进行了控制仅对pad位置做掩码保证padding token不会反向传播出无意义的梯度。优化器我选择了Adam学习率设置为0.001。因为LSTM的参数量不算特别大Adam这种自适应学习率的方法表现相当稳定不需要像SGD那样精细地调整学习率调度。不过我也加了学习率衰减每训练5个epoch将学习率乘以0.8这样后期能更精细地逼近最优位置。梯度裁剪Gradient Clipping是LSTM训练中必须要做的事我设置为max_norm5。这个技巧特别重要新闻长句子多很容易出现梯度范数突然增大的情况裁剪之后训练基本不会因为NaN中断。另外提一句Embedding训不训练的问题。基础版Seq2Seq的Embedding是随机初始化并参与训练的这也是毕设的主流做法。后续如果想做对比实验可以换成预训练的词向量比如Word2Vec输出的词向量来初始化Embedding并对比两组实验的ROUGE差异这也是一个非常容易出结果的创新点。4.2 损失函数与Mask技巧生成任务本质上是一个多分类问题所以损失函数采用交叉熵。但这里有个细节padding位置的loss需要屏蔽掉。每个batch里的摘要长短不一短句子后面补了一堆pad。如果不屏蔽这些位置模型会花大量“精力”去学习预测pad这个符号而且它占的比例可能很高严重稀释真实摘要词的梯度。def calculate_loss(logits, target, pad_idx, mask): # logits: (batch, seq_len, vocab_size) # target: (batch, seq_len) logits_flat logits.view(-1, logits.size(-1)) target_flat target.view(-1) loss F.cross_entropy(logits_flat, target_flat, ignore_indexpad_idx, reductionnone) loss loss.view(logits.size(0), logits.size(1)) loss (loss * mask).sum() / mask.sum() return lossignore_indexpad_idx已经天然帮你屏蔽了padding位置但如果还想更精细地控制比如给摘要的开头结尾不同的权重就用mask矩阵去乘。我在实际训练中直接用了ignore_index效果已经够好。4.3 训练循环细节与Early Stopping训练循环的标准流程是for epoch in range(num_epochs): model.train() total_loss 0 for batch in train_dataloader: src, src_len, trg, trg_len batch optimizer.zero_grad() output model(src, src_len, trg[:-1], src_len) loss calculate_loss(output, trg[1:], pad_idx) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm5) optimizer.step() total_loss loss.item() avg_loss total_loss / len(train_dataloader) # 每个epoch结束后在验证集上计算ROUGE val_rouge evaluate(model, val_dataloader) if val_rouge best_rouge: torch.save(model.state_dict(), best_model.pt) best_rouge val_rouge你可能注意到我对输入输出做了错位处理模型输入的trg[:-1]去掉了最后一个词而计算loss用的是trg[1:]去掉了最开始的bos。这就是Decoder训练时的“对齐”逻辑每个位置的预测目标是下一个词。Early Stopping用验证集ROUGE来控制连续5个epoch没有提升就停止训练。这个机制避免了我多跑了几十个无意义的epoch节省了时间。训练过程中我记录了一个非常有参考价值的loss曲线大约在第8个epoch时训练loss已经降到2.1左右此时验证集ROUGE-1还只有28%。继续训练到第18个epoch训练loss降到1.3ROUGE-1才涨到36%。这说明loss下降并不意味着生成质量马上变好需要耐心跑到足够多轮次再评估很多人一看loss降到2.0以下就去改模型结果白白丢掉了后面更大的提升空间。4.4 训练过程的显存和速度调优训练这条模型我在单张普通消费级显卡8GB显存上batch size只能开到64跑到第20个epoch大约需要2个小时。如果显存不够有几个降显存的实用策略减小batch_size到16或32。减少embed_size和hidden_size从256降到128。使用torch.cuda.amp混合精度训练显存占用能降低30%以上速度反而提升。梯度累积用多个小batch的梯度叠加替代一个大batch。我在最终版本里用了混合精度训练不仅显存降了训练时间还缩短了将近一半。这段经验在答辩时也很加分说明你确实在工程上下了功夫。5. 参数调优与对比实验做出扎实的论文数据5.1 超参数速查表做一个硬核的毕设超参数必须交代清楚以下是我最终实验用的配置表参数组参数名我的设置备注数据词表大小30000覆盖99%词汇数据原文最大长度200超过部分截断数据摘要最大长度50超过部分截断模型Embedding维度256可调128-512模型LSTM隐层维度256可调128-512模型LSTM层数1加深效果不一定更好模型Dropout0.3防止过拟合训练Batch Size64根据显存调整训练学习率0.001Adam默认值附近训练学习率衰减每5轮乘0.8后期精调训练梯度裁剪5.0防止梯度爆炸训练Epoch数30带早停实际约20轮收敛5.2 消融实验设计为了验证各组件的必要性我设计了四组实验Model A单向LSTM Encoder 无注意力机制的Decoder最简陋的基线。Model B双向LSTM Encoder 无注意力机制的Decoder。Model C双向LSTM Encoder 带注意力机制的Decoder完整版。Model D在模型C基础上增大词表到50000验证词表规模影响。实验结果出来Model C在ROUGE-1上比Model B高出约6个百分点比Model A高出约9个百分点。这个差距量级基本上就是注意力机制的“贡献证明”写论文时很有说服力。除了ROUGE指标我还对每个模型生成的摘要做了一个人工评价主要看“是否包含新闻核心实体”“语句是否通顺”“是否存在事实错误”。这个“人工评测”环节虽然是加分项但对毕设来说非常重要因为ROUGE衡量的是字面重合度有时候和人类感受并不完全一致。5.3 ROUGE评估指标详解ROUGERecall-Oriented Understudy for Gisting Evaluation是摘要生成任务中最常用的自动评估指标。其中ROUGE-1用来衡量单个词的匹配情况ROUGE-2衡量两个连续词的匹配情况ROUGE-L根据最长公共子序列LCS来计算匹配。计算公式并不复杂拿ROUGE-1举例ROUGE-1 系统生成摘要和参考摘要中共同出现的单字/词数量 / 参考摘要中总词数当然实际操作中我用的评测工具是HuggingFace的datasets库里的load_metric(rouge)几行代码就能算出多个指标。这里有一个测评细节中文ROUGE计算一般基于字而不是分好的词因为中文分词工具在评估时可能会引入额外误差。我同时跑了基于分词和基于字符的ROUGE然后在论文里统一采用基于字符的结果理由是更接近语言模型的真实评估方式。6. 常见问题与排查技巧实录这一章是整篇内容里最“值钱”的部分因为上面那些都是“顺利版本”实际跑项目的过程中遇到的坑比想象中多得多。6.1 Loss下降缓慢甚至不降最常遇到的状态。排查思路按照可能性排序检查数据是否有问题。我之前清洗数据时漏了一步语料里有大量空行和只包含标点的“假句子”导致模型训练时看到的输入几乎都是无意义符号。加过滤规则后loss从2.5直接降到1.8。检查词表是否包含大量未登录词。如果unk的频率特别高说明切词逻辑有误导致模型的基本输入质量很差。检查学习率。如果学习率过小比如0.00001模型几乎原地踏步先把学习率调到0.001看看loss曲线斜率。检查标签对齐。Decoder的输入和输出错位的时候模型相当于在学“预测当前位置的词”对齐错误会直接让loss居高不下。6.2 预测出来的摘要全是eos或unk这是Decoder在目标端词汇空间没学好的典型表现。核心原因通常是词表只编码了源端数据没有把摘要中的词汇也纳入词表统计。比如你按正文的词频构建词表但很多摘要用词在正文中很少出现导致这些词在词表里根本不存在生成时自然只能输出unk。正确做法是词表必须基于“正文摘要”的合并语料构建。这一点非常重要我一开始就是只统计了正文词频导致摘要里常见的时间词“近日”“昨日”全变成了unk生成效果惨不忍睹。6.3 Beam Search生成重复词生成摘要时发现某个词反复出现比如“我国我国我国经济持续发展”。这个问题的原因通常是对应位置的语言模型概率分布过于尖锐模型陷入局部循环。处理办法有几个维度在Beam Search内部加入重复惩罚项每当一个词已经在当前候选序列中出现过就将其分数乘以一个小于1的惩罚因子。降低Beam Search的宽度从5降到3重复现象会减轻不少。增加Decoder Dropout让模型在训练时看到更多多样性降低对个别词的过度自信。6.4 显存溢出OOM这是一道经典拦路虎。除了前面提到的混合精度、减小batch等还有一个专门针对Seq2Seq的操作使用梯度累积模拟更大的batch size。我在显存不足时采用了4个batch累积一次更新效果和直接用大batch几乎一样。还有一个隐蔽的显存杀手动态padding没做对。如果整个验证集都填充到固定长度再喂入模型显存占用会瞬间翻倍。改用pack_padded_sequence后这个问题彻底解决。6.5 困惑度很低但生成的摘要可读性差这个现象的原因要从训练目标和生成目标之间的差异说起。训练时用Teacher Forcing目标是让每一步都预测得准但推理时上一步的错误会传导到下一步导致误差累积。解决办法是把训练阶段的Teacher Forcing比例逐步降低比如前10轮100%强制教学后10轮随机使用真实词和预测词或者使用一种叫SeqGAN/强化学习的方法直接优化ROUGE但后者训练难度较高不太建议在毕设里硬上。7. 一点心得与可扩展的方向现在回头复盘这个毕设项目我认为最值得分享的心得是不要迷信复杂的模型要把基础框架吃透、把数据弄干净、把实验做完整。很多人一上来就翻出Transformer的源码看完一大堆多头注意力但连LSTM的隐状态形状都说不清楚最后训练时各种报错反而乱了节奏。把Encoder-Decoder框架彻底弄懂不仅是为了应付毕设后面学习Transformer、GPT、T5这些主流模型时你会有一种“原来万变不离其宗”的感觉。如果我是在做这个项目时回头看还会在以下三个方向再多做一些尝试指针生成网络Pointer-Generator Network在Decoder输出时增加从原文中“复制”词汇的概率特别适合新闻摘要这种“重要实体名必须原样出现”的任务。覆盖机制Coverage Mechanism记录解码器已经关注过原文哪些位置避免重复生成。预训练模型微调用中文BART或T5作为基础模型在新闻摘要数据集上微调效果大概率会碾压自己训练的LSTM。但注意如果毕设指导老师更看重“你自己实现模型”的过程那么预训练微调更适合作为“扩展实验”而非“主力内容”。最后再分享一个小技巧训练过程中把每个epoch生成的摘要样例打印出来肉眼观察变化。我就是在第12个epoch时发现模型开始学会“先提取核心事件、再补充时间地点”的结构那一刻的成就感比单纯看到指标上涨要强烈得多。希望这个东西也能帮你少走一些弯路顺顺利利把毕设做完。全文完