
又到了一年一度的毕设季后台好几个读者问我同一个题目“基于深度学习的新闻摘要生成算法实现”。这个题选得挺聪明新闻摘要属于文本生成里最有代表性的任务之一数据好找、效果直观、还能往上堆注意力机制和预训练模型不管是本科还是硕士都有足够的发挥空间。但真上手你会发现单是“能用Encoder-Decoder跑通一个模型”和“把摘要做出可用效果”之间隔着数据清洗、调参、解码策略、评估指标好几座大山。这篇文章就把我做这个题目时踩过的坑和验证过可行的方案完整梳理一遍。从模型选型、数据处理、训练细节到推理解码和评估尽量说清楚每一步“为什么这么干”不光是给代码。适合正在做相关毕设的同学也适合想认真入坑自然语言生成的新手。看完你至少能少走一个月的弯路。1. 整体方案设计与核心思路拆解1.1 为什么新闻摘要适合做深度学习毕设选毕设题目有个朴素的标准数据要够多、任务要够明确、效果要能被看懂。新闻摘要刚好三条全占。新闻语料在网上有大量公开来源爬下来就是现成的“原文-标题”配对不需要像很多 NLP 任务那样请人标注天然就是弱监督数据。摘要这个东西普通人一眼就能看出好不好答辩演示的时候老师哪怕不懂深度学习也能直观感受到“这个模型生成的标题通不通顺、有没有抓住重点”。另外这个题目的技术含量也可以循序渐进。最基础的版本用 Encoder-Decoder 注意力就能跑想拿高分可以加 Pointer-Generator 解决未登录词问题、加 Beam Search 改善生成质量、甚至换 BART/T5 这类生成预训练模型作为对比实验。一套下来从原理到工程都有了写到论文里的“实验对比”部分也会非常好看。1.2 抽取式还是生成式方案选型的关键决策做摘要之前首先要搞清楚一件事你是要做抽取式Extractive还是生成式Abstractive。这两个方向的技术路线完全不同。抽取式是从原文里挑出关键句子拼成摘要技术上更像是“排序问题”经典算法有 TextRank、基于图的排序等。它的优点是结果一定通顺、没有幻觉但缺点也很明显——摘要是原文句子的拼接不够凝练。比如一条 500 字新闻好的标题可能是一句原文里根本没有的话抽取式做不出来。生成式则是“理解原文之后重新写一句话”跟人类写标题的思路更接近。它需要模型具备一定的“改写”和“压缩”能力这正是 Encoder-Decoder 框架擅长的。毕设题目既然点名了 Encoder-Decoder那主线当然走生成式。不过实操里我会建议你做一个抽取式作为 Baseline比如简单的 TextRank 或者抽取第一句拼标题拿来和深度学习模型对比。别小看这个 Baseline它能从两个角度帮你第一毕业论文里需要对照实验Baseline 的存在让你的深度学习模型提升有了参照第二如果深度学习模型连 TextRank 都打不过你也能早点发现问题而不是拖到最后。1.3 Encoder-Decoder 为什么是文本生成的地基说 Encoder-Decoder 之前先想想“生成摘要”这件事的本质。输入是一篇几百词甚至上千词的新闻正文输出是一句几十词以内的标题模型要做的核心事情是先把长的东西“压缩理解”再把这个理解“展开成一句话”。这正好对应两个模块。Encoder 负责把整个输入序列“看一遍”编码成一个包含语义信息的向量序列Decoder 负责从这个向量序列出发一个词一个词地把摘要生成出来。你可以把整个过程类比成人类做阅读理解先读完整篇文章在脑子里形成“这篇文章在说什么”的把握然后动笔写一句话梗概——写的时候每写一个词都会回顾一下原文的重点确保自己没跑偏。这个流程在数学上的表达就是Encoder 将输入序列 x (x₁, x₂, ..., xₙ) 编码为隐状态序列 h (h₁, h₂, ..., hₙ)Decoder 在第 t 步根据之前的输出词 y₁, y₂, ..., yₜ₋₁ 以及编码信息预测下一个词 yₜ 的概率分布 p(yₜ | y₁, ..., yₜ₋₁, x)整个模型训练的目标就是让每一步的条件概率乘积尽可能大也就是最大化 P(y₁, y₂, ..., yₘ | x) 。这个过程听起来不复杂但从这个基础框架出发能延伸出注意力机制、复制机制、指针网络等大量改进方案所以我一直觉得把 Encoder-Decoder 吃透比直接调包用现成的摘要模型更有价值写论文的时候也更有话可说。2. 核心技术拆解从RNN到注意力再到复制机制2.1 编码器用双向LSTM把新闻读进隐状态在最基础的版本里Encoder 用的是单层 LSTM但我在实际项目里强烈建议你用双向 LSTMBiLSTM。原因很简单新闻标题的写作经常会用到全文前后文的信息。举个例子“球队在落后 20 分的情况下完成逆转”如果你想在摘要里准确体现“逆转”这个动作只看前半句是不够的必须结合后文才知道发生了什么。双向 LSTM 的 forward 方向能捕捉上文信息backward 方向能捕捉下文信息把两者拼接起来每个位置的隐状态就同时携带了上下文信息这对摘要任务来说非常关键。具体参数上我当时的设置是词向量维度 dₑₘ₆ 256LSTM 隐藏层维度 dₕᵢddₑₙ 512。注意这里有个小坑——如果你用 PyTorch 的 nn.LSTM需要注意 batch_first 参数。我习惯设置 batch_firstTrue这样输入张量的形状是 (batch_size, seq_len, embedding_dim)数据的组织方式更符合直觉。编码器输出的隐状态 hᵢ 会组成一个矩阵维度是 (seq_len, hidden_dim * 2)双向所以要乘 2。这个矩阵后半部分的工作——也就是注意力机制——要靠它提供原始素材所以这里尽量别省显存hidden_dim 设成 512 是我试下来成本和效果比较平衡的点。2.2 解码器与注意力机制每次生成都回顾原文重点如果只有一个编码器的最后一个隐状态传给解码器模型就相当于逼着一个几百词的新闻“压缩成一个向量”这对长文本来说信息损失太严重了而且解码器越到后面对原文的记忆越模糊。解决方案就是注意力机制。它的核心思想是解码器在生成每个词的时候不是机械地依赖一个固定向量而是动态地“回看”原文的每一个位置找出当前步骤最应该关注的部分。我使用的是经典的 Bahdanau Attention也叫加性注意力。它的计算过程可以拆成三步解码器当前隐状态 sₜ 和编码器的每个隐状态 hᵢ 做一个打分函数得到一个分数 eₜᵢ vᵀ tanh(W₁hᵢ W₂sₜ)对所有分数做 softmax得到注意力权重 αₜᵢ用权重对所有编码器隐状态做加权求和得到上下文向量 cₜ Σᵢ αₜᵢ hᵢ然后把这个上下文向量和解码器隐状态拼接起来用于预测下一个词。这个机制就类似于人写作时“重读原文相关段落”的行为注意力权重越大说明原文那个位置对当前生成词的贡献越大。我经常跟人说注意力机制的美妙之处在于它不光带来了效果提升还能可视化——训练完把注意力权重画出来你能直接看到模型在生成“冠军”这个词时确实在重点看原文里“夺冠”那句话这玩意拿到答辩现场演示效果拉满。2.3 指针生成网络解决未登录词问题新闻里人名、机构名、地名密集出现比如“某跨国公司的 CEO 宣布了什么政策”这些专有名词往往不在词表里是未登录词OOV。标准的 Seq2Seq 模型遇到这种情况只有一个办法生成一个UNK占位符。如果摘要里频繁出现UNK整个输出就废了老师一看就是“模型没训练好”。解决方案是在基础架构上加入指针生成网络Pointer-Generator Network。它的核心思想是在生成每个词时模型先算一个“拷贝概率”p_gen这个概率表示当前这个词是从词表里“生成”的还是直接从原文里“复制”过来的。具体来说解码器每一步会计算 p_gen σ(Wc · cₜ Wy · yₜ Ws · sₜ b)其中 yₜ 是当前输入词。然后最终的词概率分布是P(w) p_gen × P_vocab(w) (1 - p_gen) × Σᵢ:ᵢ₌ᵥ αₜᵢ这个式子右边第一项就是“从词表生成词”第二项是把原文里出现的词按注意力权重“复制”过来。有了这个机制新闻里的人名地名就能原封不动地出现在摘要里效果提升非常明显。我在实验里对比过指针网络把 ROUGE-1 的分数提高了大约 4 到 5 个点而且是肉眼可见的改善——没有它摘要里一遇到人名就出现UNK有了它摘要瞬间像样多了。2.4 覆盖机制防止摘要陷入循环重复生成模型有个臭名昭著的问题生成到后面模型经常会重复刚才已经说过的词比如“某某球队 获胜 获胜 获胜”。这是因为解码器在生成第 t 个词时没有“记忆”自己之前已经覆盖过哪些原文信息注意力可能反复落在同一个位置。解决办法是 Coverage Mechanism覆盖机制。做法是维护一个覆盖向量 covᵢ表示编码器第 i 个位置已经被注意力“看过”多少轮。每次计算注意力打分时把这个覆盖向量加权进去相当于对重复关注过的位置进行“惩罚”eₜᵢ vᵀ tanh(W₁hᵢ W₂sₜ W₃covᵢ)同时在损失函数里加一个覆盖损失项鼓励注意力权重不要重复分配。加了覆盖机制之后模型生成重复词的频率显著下降尤其是在新闻这种信息密集的文本上效果非常值得做。实测下来覆盖机制 指针网络这两个改进是毕设拿高分的关键组合。3. 数据处理与模型训练的实操细节3.1 数据集选择与清洗流程做摘要任务数据质量基本决定模型效果的天花板。我在做的时候用的是某公开的中文新闻语料库里面有几十万条新闻每条包含“正文”和“标题”两个字段。但原始数据不能直接用里面的脏数据比你想象的多得多。我梳理了一套清洗流程你直接照着做就行:去掉正文长度小于 100 的条目太短的新闻没有摘要价值而且会给数据引入大量噪声去掉正文和标题长度比小于 2 的条目正常新闻的标题应该远比正文短如果标题比正文还长这个数据八成有问题删除含 HTML 标签、广告关键词如“点击查看”“了解更多”的文本用正则表达式一次性处理全文统一转换成简体中文这里我用到了 OpenCC 库繁简转换比较可靠把标题里的引号、书名号统一成半角格式避免同一个词因为引号不同被切成两个 token这套流程走完数据从最初的 40 万条左右清洗到 32 万条左右清洗率大约 20%。别嫌洗掉得多摘要模型对数据的整齐度极其敏感一条带广告尾巴的样本就能让模型在验证集上多出两三个“点击查看”之类的垃圾输出。3.2 分词、构建词典与数据截断中文 NLP 跟英文有个关键区别英文天然按空格分词中文必须先分词。我试过 jieba 分词和基于 BERT 的切词实际使用中 jieba 就够用了毕竟我们用的是 LSTM 这类传统模型不是预训练模型。分词之后是构建词典。这里要给一个最低配置参考词表大小设成 5 万把出现次数低于 5 的词归入UNK。构建词典时一定要把PAD、UNK、BOS、EOS这四个特殊 token 预留出来。其中PAD用来把 batch 里长度不一的句子补齐到相同长度UNK代表未登录词BOS和EOS分别用于告诉解码器“开始生成”和“结束生成”。关于截断长度我做了一组简单的统计分析新闻正文长度集中在 300 到 600 字标题集中在 15 到 50 字。所以我设置了正文最大长度 512标题最大长度 80。长度不够就 pad超过就截断。这个设定在显存利用率和效果之间比较平衡建议你跟着自己数据的分布去统计一下而不是拍脑袋随便定。3.3 模型参数与训练策略模型结构搭建完成之后关键就是怎么训练。我把自己的参数配置整理成了表格照着跑基本不会有方向性的错误参数设置值说明词向量维度256随机初始化没有用预训练词向量LSTM 隐层维度512编码器是双向所以实际维度 1024LSTM 层数2超过 2 层提升有限训练成本成倍增加Batch Size64视显存调整12G 显存够用优化器Adam学习率 0.001注意配合梯度裁剪梯度裁剪5.0防止 RNN 梯度爆炸必须加学习率衰减每 2 个 epoch 乘以 0.8后期收敛更稳定Teacher Forcing 比例1.0 开始逐步降低前期强制用真实词后期让模型自己生成Epoch 数15 左右根据验证集 ROUGE 早停这里重点说两个只有实际跑过才会懂的坑。第一个坑是Teacher Forcing。Decoder 训练时一种做法是每一步都把模型上一步生成错误的词喂回模型结果误差会像滚雪球一样越滚越大模型根本训练不动。所以我用 Teacher Forcing训练时以一定概率把真实的目标词喂给解码器作为下一步输入而不是用模型上一步生成的词。但在训练后期要逐步降低这个比例让模型逐渐学会“自己生成自己吃”否则训练出来的模型在测试时会因为推理和训练分布不一致而崩掉。第二个坑是损失函数只计算 Decoder 部分的损失。新手很容易把 Encoder 的损失也算进去但实际上 Encoder 没有直接的监督信号它只是把输入编码成向量损失全部来自解码器每个 step 的交叉熵。实现时记得把 padding 位置的损失 mask 掉不然PAD位置会让损失函数出现偏差。我见过有人没做 padding mask训练了十几个 epoch 后 loss 还是降不下去查了半天发现是这个问题。3.4 核心代码框架一个简化但完整的实现下面是训练循环的核心框架代码基于 PyTorch我做了删减保留关键部分。要注意这里只是让你看懂数据流完整代码里还需要补上注意力打分、覆盖向量这些细节class Encoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, num_layers2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idx0) self.lstm nn.LSTM(embed_size, hidden_size, num_layers, batch_firstTrue, bidirectionalTrue) def forward(self, src, src_len): embedded self.embedding(src) # [batch, seq_len, embed_size] packed nn.utils.rnn.pack_padded_sequence( embedded, src_len, batch_firstTrue, enforce_sortedFalse) outputs, (hidden, cell) self.lstm(packed) outputs, _ nn.utils.rnn.pad_packed_sequence(outputs, batch_firstTrue) # outputs: [batch, seq_len, hidden_size * 2] return outputs, hidden, cell class Decoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, attention, num_layers2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idx0) self.attention attention self.lstm nn.LSTM(embed_size hidden_size * 2, hidden_size, num_layers, batch_firstTrue) self.fc_out nn.Linear(hidden_size * 3, vocab_size) def forward(self, trg, encoder_outputs, hidden, cell): embedded self.embedding(trg) # [batch, trg_len, embed_size] # 将编码器输出展平到每个时间步 outputs [] for t in range(trg.size(1)): step_input embedded[:, t:t1, :] # [batch, 1, embed_size] context, attn_weights self.attention(step_input, encoder_outputs) lstm_input torch.cat((step_input, context), dim-1) output, (hidden, cell) self.lstm(lstm_input, (hidden, cell)) output torch.cat((output, context), dim-1) pred self.fc_out(output) # [batch, 1, vocab_size] outputs.append(pred) return torch.cat(outputs, dim1)上面代码里我特意省略了注意力实现因为注意力机制根据覆盖机制的有无会有几个不同的版本。但你要看清楚核心逻辑Decoder 每个时间步都要把上一步的隐状态、当前输入词向量、注意力机制产生的上下文向量这三者拼接之后再经过 LSTM 和全连接层才能得到当前词的概率分布。这种“拼接输入”的模式是解码器的标准玩法。训练循环里有个关键点数据要装成 DataLoader但要处理变长序列。我的做法是在 collate_fn 里对 batch 内的样本按长度排序然后用 pack_padded_sequence 压缩 LSTM 计算。一句话总结就是RNN 处理变长序列时直接 pad 到相同长度会浪费大量算力pack_padded_sequence 才是正解。很多人图省事直接 pad结果训练速度慢一倍显存还爆了。4. 推理阶段、评估指标与调优实战4.1 用Beam Search而不是贪心搜索训练完成之后生成摘要的推理阶段同样有很多门道。最朴素的解码方式是贪心搜索每一步都选概率最大的词然后把这个词作为下一步输入。这种办法简单但问题在于每一步最优不等于整句话最优。类似于下棋只走当前最好的一步可能后面全崩了。我用的是 Beam Search思路是每一步保留 top-k 个候选序列k 叫 beam size一般取 4 或 5最后从候选中选出概率乘积最大的一条。这就好比一次走棋保留几种最好的走法同时往下推演。具体操作上需要注意 trg 端要按步数循环维护一个“候选序列集合”每轮扩展这个集合然后截取 top-k。这里贴上核心的推理逻辑def beam_search(model, encoder_outputs, src_mask, beam_size4, max_len80): batch_size encoder_outputs.size(0) # 初始化解码器状态和起始tokenBOS decoder_input torch.tensor([[BOS_IDX]] * batch_size) hidden, cell model.decoder.init_state(encoder_outputs) # 每个样本维护多个候选: (log_prob_sum, sequence) sequences [[0.0, [BOS_IDX]]] completed [] for step in range(max_len): all_candidates [] for log_prob, seq in sequences: decoder_input torch.tensor([seq[-1]]).unsqueeze(0) output, hidden, cell model.decoder( decoder_input, encoder_outputs, hidden, cell) log_probs torch.log_softmax(output[0, -1], dim-1) topk_log_probs, topk_idx torch.topk(log_probs, beam_size) for k in range(beam_size): candidate (log_prob topk_log_probs[k].item(), seq [topk_idx[k].item()]) all_candidates.append(candidate) # 按得分排序保留当前长度下最好的beam_size个 all_candidates.sort(keylambda x: x[0], reverseTrue) sequences [] for _, seq in all_candidates: if seq[-1] EOS_IDX: completed.append((log_prob, seq)) else: sequences.append((log_prob, seq)) if len(sequences) beam_size: break if len(sequences) 0: break if completed: best sorted(completed, keylambda x: x[0], reverseTrue)[0] else: best sorted(sequences, keylambda x: x[0], reverseTrue)[0] return best[1]这里有一个容易出错的细节Beam Search 的 log 概率用的是累加。因为每个词的概率都小于 1连乘会让数值越来越小甚至下溢所以统一取 log 之后做加法。另外beam 里所有序列尽量保持在相近的长度避免短的序列因为 token 少而得分偏高所以在最终排序时我会对长度做归一化具体做法是除以 sequence length 的某个幂次比如 0.7这个幂次是经验值。4.2 ROUGE指标到底怎么算如何看懂它的含义做摘要毕设评估指标绕不开 ROUGE。ROUGE 的全称是 Recall-Oriented Understudy for Gisting Evaluation简单理解就是把生成的摘要和参考摘要拿来对比重叠词的数量。它有几个变种ROUGE-1单个词的匹配率ROUGE-2相邻两个词bigram的匹配率ROUGE-L最长公共子序列的匹配率不要只盯着一个指标看。我的经验是ROUGE-1 高不一定说明摘要好因为它只看单个词模型只要多输出几个高频词就能刷上去ROUGE-2 更严格需要连续两个词都匹配ROUGE-L 关注的是语序信息。三个指标放在一起看才能反映模型的真实水平。实操里我直接用 py-rouge 或者 HuggingFace 的 evaluate 库里面已经封装好了计算逻辑。基线水平大致这样在一个几十万规模的中文新闻数据集上简单的抽取式 Baseline取正文第一句做标题ROUGE-1 能有 20 上下。我的指针生成网络 覆盖机制模型ROUGE-1 到了 31 左右ROUGE-2 在 17 左右ROUGE-L 在 27 左右。别小看这个数字新闻标题的写作自由度高同一个新闻不同媒体可能起完全不同的标题所以 ROUGE 分数天花板没有想象中那么高ROUGE-1 能到 30 以上已经算个不错的模型了。4.3 常见问题排查速查表做这个项目时遇到的 90% 的问题都可以归到下面几类。我直接整理成一张表你碰到类似现象对着查就行问题现象可能原因排查与解法Loss 不下降学习率过高或过低尝试 0.001、0.0005 从小往大调观察前 500 步趋势Loss 下降到 3 左右就停滞没有做 padding mask检查交叉熵是否把 位置的损失也算进去了生成摘要通篇都是 未登录词问题加入指针生成网络或增大词表生成内容重复严重注意力崩溃加入覆盖机制第 2 个 epoch 之后开始生效生成摘要比原文还长没有设置 终止条件Beam Search 里强制遇到 or 达到 max_len 就停止训练时显存溢出batch size 太大从 64 降到 32或开启梯度累积验证 ROUGE 高但人工看起来差训练数据太短检查数据集里是不是有大量只能提取原句的短新闻同一个 batch 内长度差异巨大训练慢没有用 pack_padded_sequence改用长度排序 pack 机制其中我想展开说两个最容易被忽视的坑。第一个是验证集不干净导致 ROUGE 虚高。我遇到过一次验证集里混了一些从网上抓的“脏数据”标题就是正文第一句话的截断导致模型只要机械地复制开头ROUGE 就非常高。但拿到真实新闻上一测效果惨不忍睹。这个问题必须警惕验证集也得经过清洗。第二个是训练和验证的预处理不一致。我一度在训练时做了繁转简和全角转半角但验证时因为脚本写漏了全角转半角导致所有分数掉了 2 到 3 个点。看起来小实际影响巨大这种低级的预处理不一致问题我建议你在跑实验之前写一个单测脚本验证 train/val 的预处理输出是否一致。5. 实验设计、论文展示与答辩经验5.1 对比实验怎么设计才好看毕设能不能拿高分一半在模型的创新度另一半在实验设计的完整度。单跑一个模型是不行的你需要一个递进式的实验序列。我当时设计的四组实验是Baseline抽取式方法比如 TextRank 或直接取首句用于证明“简单方法”的上限基础 Seq2Seq Attention证明深度学习方法至少不输给抽取式Seq2Seq Attention Pointer-Generator证明复制机制对新闻摘要至关重要上面全部 Coverage 机制证明覆盖机制能解决重复问题四组实验结果画成折线图一组比一组高而且每一组提升都有明确的原理解释。答辩的时候老师问“你这个模型对比抽取式有什么优势”“为什么加 Pointer 有用”你都能拿出数据逐条讲。这种“递进式”实验设计比只放一个最终模型的单项结果有说服力得多。另外我强烈建议做一组人工评估。随机抽 100 条新闻让 3 个人打分1 到 5 分评估维度是流畅度、信息完整度、概括准确度。人工评估的好处是能捕捉到 ROUGE 漏掉的语义信息比如生成标题是否和原文事实矛盾。这部分内容写进论文里能体现你对问题的全面思考而不是只会跑指标。5.2 可视化与系统演示的加分项很多同学把精力全放在模型上结果演示环节只会开个 Jupyter Notebook 跑代码给老师看那效果肯定大打折扣。我建议做一个简单的 Web 演示页面用 Gradio 或 Flask 包一个界面。用户输入新闻正文点击提交模型自动生成标题。再顺手把注意力热力图显示出来让老师看到生成某个词时模型到底在看原文哪个位置。这一步不需要特别复杂的工程但交互式的效果远胜于命令行输出。另外一个细节把生成失败的案例也准备几个。答辩现场如果全是成功案例老师会觉得你挑了好的展示准备 2 到 3 个反面案例并主动解释失败原因比如“文本太长了长度超过训练分布”“出现了训练集里没有的地名”反而能证明你对模型边界有清晰的认识。5.3 从基础版本到进阶方向如果你的时间充裕可以在基础模型之上再探索一到两个进阶方向这在论文里作为 Future Work 或者扩展实验都很有分量。用生成预训练模型做对比比如 BART 或 T5在中文上可以用中文版的生成模型。现在很多开源模型都已经支持新闻摘要任务它们的生成效果会显著超过从头训练 LSTM但参数量大、训练慢。你不需要复现整个预训练过程只需要做微调对比。加入关键词信息在很多新闻场景中标题其实就是最能概括全文的几个关键词的组合。可以尝试用无监督方式抽取关键词把它们作为辅助输入注入 Decoder。多任务联合训练比如同时让模型做“新闻分类”和“标题生成”共享 Encoder两个任务互相促进。这在论文里属于方法论创新可以增加理论深度。5.4 我自己踩过的那些坑经验总结做这个题目的过程中有几个事情如果我一开始就知道能省出至少一周时间。整理成几条个人体会放在这里第一不要在训练脚本上过度纠结代码优雅。我一开始把所有模块抽象成六个文件结果每次改一个超参数都要跳来跳去。后来我直接把训练循环、模型定义、数据处理放在三个文件里简单清晰跑实验反而快很多。毕设的核心是验证想法不是写工程框架。第二每次实验必须保存完整的配置记录。我用的是最笨的办法——每次跑实验把参数自动保存成一个 JSON 文件命名里带上日期和关键参数。否则你跑十组实验之后根本分不清哪个结果对应哪组参数。第三要尽早检查生成结果而不是只盯着 loss。Loss 下降不等于生成质量变好。我建议第一个 epoch 跑完就手动生成几条摘要看一下哪怕模型还很烂至少能确认 pipeline 是通的。我曾经历过 loss 正常下降但生成结果全是重复词的情况等发现问题的时候已经白跑了三四个 epoch。最后再分享一个小技巧如果你在训练时发现显卡显存不够用又不想降 batch size 影响收敛速度可以用梯度累积——每几个 batch 累加梯度再更新一次。我当时把 batch size 降到 32用梯度累积 2 步等效 batch size 还是 64效果几乎没有下降。但要注意用了梯度累积学习率可能需要同步微调我自己的经验是等效 batch size 翻倍时学习率可以稍微加大 10% 到 20%收敛速度会快一点。