Transformer文本摘要生成:从编码器到解码器的工程实践与避坑指南

发布时间:2026/10/20 2:26:48
Transformer文本摘要生成:从编码器到解码器的工程实践与避坑指南 简介这份资源是面向本科毕业设计场景的深度学习文本摘要自动生成项目聚焦自然语言处理与Transformer模型的工程落地适合具备一定Python基础、希望系统实践NLP课程设计或毕业设计的学生。压缩包共34个文件约360KB以18个Python脚本为核心覆盖模型定义、数据预处理、训练与评估流程另含5个Shell脚本用于训练与测试调度以及txt、yml、vocab、Dockerfile等配置与词表文件整体结构清晰、便于按模块阅读。项目围绕编码器-解码器架构展开涉及分词、词汇表构建、序列化输入、损失函数与优化器配置并引入ROUGE、BLEU等指标衡量摘要质量同时提供beam search、强化学习训练等进阶实现可帮助读者理解从数据到模型再到评价的完整链路。目前已有3764人学习下载适合作为NLP方向毕业设计的参考方案与代码模板。1. 文本摘要自动生成为什么模型写出的摘要总像“正确的废话”你大概见过这样的场景把一篇两千字的新闻丢给一个摘要模型它返回一句“本文介绍了某事件的发生背景、主要经过和各方反应”。语法没错信息也没错但读完等于没读。这不是模型偷懒而是文本摘要这个任务本身有一个反直觉的底层矛盾——摘要的“好”没有唯一解但训练时模型只能对着一个参考答案学。抽取式摘要从原文里挑句子看起来安全却容易选出冗长、重复、指代不明的片段生成式摘要能压缩改写却要面对“事实一致性”这个玄学问题模型可能把“A 公司收购 B 公司”写成“B 公司收购 A 公司”句子通顺事实翻车。本科毕业设计选这个方向真正要搞懂的不是调哪个库而是 Transformer 到底怎么把一篇长文本压进一组向量、解码器又怎么把这组向量重新展开成通顺句子。这份资源围绕的正是这条从“理解”到“生成”的完整链路把注意力机制、编码器-解码器结构、训练目标和推理策略串成一条能跑通的工程路径。搞懂它你再看任何摘要模型都能一眼看出它是在哪个环节妥协了。2. Transformer 编码器一篇长文本是怎么被压进向量里的2.1 自注意力到底在算什么文本摘要的第一个硬骨头是“长距离依赖”。传统 RNN 按顺序读词读到句尾时句首的信息已经衰减得差不多了CNN 靠卷积核扩大感受野但层数不够时仍然抓不住跨段落的关联。Transformer 的自注意力机制换了个思路每个词直接和序列里所有词算相关性一步到位建立全局连接。具体做法是把每个词向量投影成三个矩阵Query查询、Key键、Value值。用公式说就是Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) VQ 和 K 的点积衡量“这个词该对那个词放多少注意力”除以 sqrt(d_k) 是防止维度太高时点积过大、softmax 梯度消失。softmax 归一化后得到权重再对 V 加权求和输出就是融合了全局信息的新的词表示。我一般会这么理解Query 是“我在找什么”Key 是“我有什么”Value 是“我实际能提供的内容”。三者都来自同一个输入序列时叫自注意力如果 Query 来自解码器、Key 和 Value 来自编码器那就是交叉注意力负责在生成时“回头看原文”。2.2 多头注意力为什么要拆成好几组单组注意力只能学到一种关联模式。有的词需要关注语法搭配有的需要关注语义指代一组权重很难同时兼顾。多头注意力把 d_model 维的向量切成 h 份每份独立做一次注意力最后拼接再线性变换。import torch import torch.nn as nn class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super().__init__() assert d_model % num_heads 0 self.d_model d_model self.num_heads num_heads self.d_k d_model // num_heads # 三个投影矩阵分别生成 Q、K、V self.W_q nn.Linear(d_model, d_model) self.W_k nn.Linear(d_model, d_model) self.W_v nn.Linear(d_model, d_model) self.W_o nn.Linear(d_model, d_model) def forward(self, query, key, value, maskNone): batch_size query.size(0) # 投影后拆成多头: (batch, seq_len, num_heads, d_k) - (batch, num_heads, seq_len, d_k) Q self.W_q(query).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) K self.W_k(key).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) V self.W_v(value).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) # 缩放点积注意力 scores torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5) if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn torch.softmax(scores, dim-1) context torch.matmul(attn, V) # 拼接多头并做输出投影 context context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model) return self.W_o(context)这段代码里最关键的参数是num_heads。头数太少模型只能学到单一维度的关联头数太多每个头的维度 d_k 太小表达能力反而下降。常见的做法是 d_model512、num_heads8每个头 64 维。mask 参数在编码器里用来屏蔽 padding 位置在解码器里还要额外屏蔽未来位置防止生成时“偷看”后面的词。2.3 位置编码没有它注意力就是词袋自注意力本身对词序不敏感——把“猫追狗”和“狗追猫”打乱输入算出来的注意力权重完全一样。这显然不行。Transformer 用位置编码把顺序信息注入进去PE(pos, 2i) sin(pos / 10000^(2i/d_model)) PE(pos, 2i1) cos(pos / 10000^(2i/d_model))用正弦余弦而不是可学习的位置嵌入好处是能外推到训练时没见过的序列长度。每个位置的位置编码和词嵌入直接相加这样每个词的表示里既包含语义信息也包含它在序列中的位置信息。编码器通常堆 6 层每层包含一个多头自注意力子层和一个前馈网络子层两个子层都带残差连接和层归一化。残差连接保证梯度能直接回传层归一化稳定训练过程。堆叠多层的作用是逐层抽象底层关注局部搭配高层关注段落级语义。3. 解码器与训练目标摘要句子是怎么一个词一个词“长”出来的3.1 掩码自注意力与交叉注意力解码器的结构和编码器类似但多了两个关键设计。第一是掩码自注意力生成第 t 个词时只能看到前 t-1 个词不能看到后面的。实现上就是在注意力分数矩阵的上三角区域填负无穷softmax 之后这些位置的权重变成 0。第二是交叉注意力Query 来自解码器当前状态Key 和 Value 来自编码器输出。这一步让解码器在生成每个词时都能“回看”原文的不同部分决定当前该关注原文的哪句话。class DecoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout0.1): super().__init__() self.self_attn MultiHeadAttention(d_model, num_heads) self.cross_attn MultiHeadAttention(d_model, num_heads) self.ffn nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model) ) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.norm3 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, enc_output, src_mask, tgt_mask): # 掩码自注意力只看已生成的词 attn_out self.self_attn(x, x, x, tgt_mask) x self.norm1(x self.dropout(attn_out)) # 交叉注意力Query 来自解码器Key/Value 来自编码器 cross_out self.cross_attn(x, enc_output, enc_output, src_mask) x self.norm2(x self.dropout(cross_out)) # 前馈网络 ffn_out self.ffn(x) x self.norm3(x self.dropout(ffn_out)) return x注意tgt_mask和src_mask的区别前者是下三角矩阵屏蔽未来位置后者屏蔽原文的 padding。交叉注意力里传入的是src_mask因为原文所有有效位置都应该被看到。这个结构决定了摘要生成的一个核心特性——每生成一个词模型都会重新扫一遍原文而不是像 RNN 那样只靠一个固定长度的上下文向量。3.2 教师强制与损失函数训练时用“教师强制”策略把参考答案的前 t-1 个词喂给解码器让它预测第 t 个词。损失函数是交叉熵逐词计算预测分布和真实词之间的差异def train_step(model, src, tgt, optimizer, criterion): # tgt 形如 sos w1 w2 ... wn eos tgt_input tgt[:, :-1] # 输入: sos w1 ... wn-1 tgt_output tgt[:, 1:] # 目标: w1 w2 ... eos # 构造掩码 src_mask (src ! PAD_IDX).unsqueeze(1).unsqueeze(2) tgt_mask torch.tril(torch.ones(tgt_input.size(1), tgt_input.size(1))).bool() logits model(src, tgt_input, src_mask, tgt_mask) # 展平后计算交叉熵忽略 padding 位置 loss criterion(logits.reshape(-1, logits.size(-1)), tgt_output.reshape(-1)) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()这里有个容易翻车的点criterion必须设置ignore_indexPAD_IDX否则 padding 位置也会参与损失计算模型会学会“预测 padding”浪费容量。另外教师强制会导致训练和推理不一致——推理时没有参考答案模型只能吃自己上一步的输出一旦前面生成错了错误会累积。这个现象叫 exposure bias是生成式摘要的经典问题常见缓解手段是计划采样或强化学习微调但本科阶段先把基础流程跑通更重要。3.3 推理策略贪心、束搜索与长度惩罚推理时从sos开始每步选一个词直到生成eos或达到最大长度。最简单的贪心策略每步选概率最大的词但容易生成重复、平淡的句子。束搜索保留 top-k 条候选路径最后选整体概率最高的def beam_search(model, src, src_mask, beam_size4, max_len50): # 初始只有 sos beams [([SOS_IDX], 0.0)] for _ in range(max_len): candidates [] for seq, score in beams: if seq[-1] EOS_IDX: candidates.append((seq, score)) continue tgt_tensor torch.tensor(seq).unsqueeze(0) tgt_mask torch.tril(torch.ones(len(seq), len(seq))).bool() logits model(src, tgt_tensor, src_mask, tgt_mask) log_probs torch.log_softmax(logits[:, -1, :], dim-1) topk_probs, topk_ids log_probs.topk(beam_size) for i in range(beam_size): candidates.append((seq [topk_ids[0, i].item()], score topk_probs[0, i].item())) # 按累计对数概率排序保留 top-k beams sorted(candidates, keylambda x: x[1], reverseTrue)[:beam_size] if all(s[-1] EOS_IDX for s, _ in beams): break return beams[0][0]束搜索的 beam_size 是个权衡参数太小退化成贪心太大显存和计算量成倍增长。实际项目里还会加长度惩罚因为对数概率是负数越长的序列累计值越小模型倾向于生成短句。长度惩罚通常用score / length^alpha来归一化alpha 取 0.6 到 1.0 之间。这些细节在毕业设计的实验对比里往往是拉开差距的地方。4. 数据与评估为什么你的摘要模型指标好看但读起来不行4.1 数据集构造与预处理文本摘要常用的公开数据集格式是“原文-摘要”成对出现。预处理阶段要做几件事分词、截断、构造词表、添加特殊标记。原文通常很长需要截断到模型能接受的最大长度比如 512 个 token但截断策略有讲究——直接截尾可能丢掉关键结论按段落采样或取首尾拼接更合理。def build_dataset(pairs, tokenizer, max_src_len512, max_tgt_len128): data [] for src_text, tgt_text in pairs: src_ids tokenizer.encode(src_text)[:max_src_len] tgt_ids tokenizer.encode(tgt_text)[:max_tgt_len] # 目标序列首尾加特殊标记 tgt_ids [SOS_IDX] tgt_ids [EOS_IDX] data.append((src_ids, tgt_ids)) return data词表大小直接影响模型参数量。词表太小很多词被切成子词序列变长词表太大嵌入矩阵占显存。常见做法是用 BPE 或 WordPiece 做子词切分词表控制在 3 万到 5 万之间。另外要注意原文和摘要必须用同一个分词器否则解码器输出的词和词表对不上。4.2 ROUGE 指标的局限ROUGE 是摘要任务最常用的自动评估指标核心思想是计算生成摘要和参考答案之间的 n-gram 重叠。ROUGE-1 看单词重叠ROUGE-2 看二元组重叠ROUGE-L 看最长公共子序列。公式看起来客观但问题很明显它只认字面重叠不认语义等价。模型把“经济增长迅速”写成“经济快速发展”ROUGE 分数会掉但人读起来意思一样。更麻烦的是ROUGE 高的摘要不一定事实一致。模型可能从原文不同位置拼凑出通顺但错误的句子ROUGE 照样给高分。所以毕业设计里如果只报 ROUGE答辩时很容易被问住。常见的补充做法是人工抽检几十条样本按“流畅度、信息量、事实一致性”三个维度打分哪怕样本少也能说明你意识到了指标的局限。4.3 事实一致性问题的根源生成式摘要的事实错误大致分三类实体替换人名、机构名搞混、数量错误数字、日期写错、关系颠倒主客体互换。根源在于模型学的是“给定原文什么摘要概率最高”而不是“给定原文什么摘要事实正确”。训练数据里的参考答案本身就可能包含噪声模型把噪声也学进去了。缓解思路有几个方向一是复制机制让模型在生成实体时倾向于直接从原文复制而不是从词表采样二是引入实体识别或关系抽取的辅助损失三是在解码时对实体词做约束。这些在本科阶段不一定要全部实现但在论文的“不足与改进”部分写清楚能体现你对问题本质的理解。5. 避坑与排查训练摘要模型时最容易翻车的几个地方5.1 损失降到很低但生成全是重复词现象训练集 loss 稳定下降但推理时模型反复输出同一个词或同一句话。原因解码器掩码写错导致训练时模型能“偷看”未来词学到的条件分布和推理时不一致。或者束搜索没有加长度惩罚模型陷入“重复短句累计概率高”的局部最优。解决检查tgt_mask是否是严格下三角矩阵确保第 t 个位置只能看到前 t-1 个位置。推理时加长度惩罚和重复惩罚对已生成的词降低其再次出现的概率。5.2 显存溢出batch size 调到 1 还是 OOM现象训练时 CUDA out of memory减小 batch 也没用。原因自注意力的计算复杂度是 O(n²)序列长度翻倍显存需求翻四倍。原文截断长度设成 1024 时注意力矩阵本身就占大量显存。解决把原文最大长度降到 512 或 256或者用梯度累积模拟大 batch。如果还不行考虑用稀疏注意力或分块注意力的变体但本科阶段优先降长度。5.3 ROUGE 分数异常高接近 1.0现象评估时 ROUGE-1 超过 0.9看起来好得不真实。原因评估脚本里把参考答案直接当成了生成结果或者分词时没有去掉特殊标记导致生成摘要和参考答案共享了大量sos、eos、pad标记。解决检查评估流程确保生成摘要来自模型推理输出分词时过滤掉所有特殊标记只对有效词计算重叠。5.4 训练 loss 震荡不收敛现象loss 时高时低几个 epoch 后仍然没有下降趋势。原因学习率太大或者没有用 warmup。Transformer 对学习率敏感直接上 1e-3 容易震荡。解决用 Adam 优化器学习率设 1e-4 到 5e-4加 warmup 步数比如前 4000 步线性升温。另外检查层归一化的位置Post-LN 比 Pre-LN 更难训练新手建议用 Pre-LN。5.5 生成的摘要和原文事实矛盾现象摘要读起来通顺但把原文的“增长 3%”写成“下降 3%”或者把主语和宾语搞反。原因模型过度依赖语言模型的先验生成时没有充分关注原文对应位置。交叉注意力的权重可能集中在无关词上。解决可视化交叉注意力权重看生成实体词时模型关注原文哪里。如果注意力分散考虑在训练时加入实体一致性损失或者在解码时对数字、实体词做强制复制约束。6. 从能跑到好用摘要系统的设计取舍与验证思路把模型跑通只是起点真正决定一个摘要系统好不好用的是你在几个关键维度上做的取舍。第一个取舍是抽取式还是生成式。抽取式摘要直接从原文选句子事实一致性好但句子之间的衔接生硬指代关系可能断裂生成式摘要灵活流畅但事实一致性风险高。常见的工程做法是混合式先用抽取式选出关键句再用生成式对这些句子做压缩改写。本科毕业设计如果只做生成式至少在论文里讨论一下这个取舍。第二个取舍是模型规模和推理速度。Transformer 的参数量随层数和 d_model 增长6 层 512 维的模型在单卡上还能训12 层 768 维就需要多卡或梯度累积。如果目标是部署到实际场景推理延迟比 ROUGE 分数更重要。这时候可以考虑知识蒸馏用大模型教小模型或者用层数更少的轻量结构。我一般会这么理解毕业设计的模型不需要最大但需要你能说清楚为什么选这个规模。第三个取舍是自动指标和人工评估的配比。ROUGE 适合快速迭代但不能作为唯一标准。一个可操作的验证流程是每次实验跑完 ROUGE 后固定抽 50 条样本做人工打分记录流畅度、信息量、事实一致性三个维度的均分。如果 ROUGE 涨了但人工分没涨说明模型在拟合参考答案的表面模式而不是真正学会了摘要。这个习惯一旦养成后面做任何生成任务都不会被指标带偏。还有一个容易被忽略的点是解码策略的验证。贪心、束搜索、采样各有适用场景。束搜索适合追求 ROUGE 的场景采样适合需要多样性的场景。验证方法是固定同一个模型分别用不同解码策略生成摘要对比 ROUGE 和人工分。很多时候你会发现束搜索的 ROUGE 最高但人工读起来最死板采样的 ROUGE 低一些但摘要更自然。这个对比本身就能成为毕业设计里一个有价值的实验章节。从那以后我每次做生成任务都会在跑完自动指标后强制抽一批样本自己读一遍读不通顺或者事实对不上的一律回去查注意力权重和数据预处理。这个习惯帮我避开了很多“指标好看但没法用”的坑。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询