基于seq2seq与注意力机制的问答摘要生成:从数据清洗到推理验证

发布时间:2026/10/6 14:44:18

基于seq2seq与注意力机制的问答摘要生成:从数据清洗到推理验证 简介这是一份汽车大师问答摘要与推理比赛的参赛源码与项目说明面向自然语言处理初学者、算法竞赛爱好者以及需要完成相关课程设计、期末大作业或毕业设计的计算机、数学、电子信息类专业学生。压缩包共37个文件以28个Python脚本和7个Jupyter Notebook为主另含1个Markdown说明文档整体体积仅128KB便于快速下载与本地调试。源码涵盖seq2seq与seq2seq_attention两套模型实现并扩展了Transformer、PGN等思路的Notebook演示以及数据预处理、Beam Search解码、训练与测试脚本等完整流程项目说明文档梳理了从数据清洗到模型推理的关键步骤代码模块按utils、models等目录组织结构清晰适合逐模块研读。Notebook中逐步演示了训练与推理过程可帮助读者理解问答摘要生成的细节也方便在此基础上二次开发或作为毕业设计的基线系统。目前已有99人学习浏览作为算法类参考资料具备较强的借鉴价值。1. 汽车大师问答摘要与推理本质是把长问答压成一句可落地的结论做问答摘要和推理这类比赛最怕的不是模型跑不起来而是拿到数据后不知道输入到底该喂什么。汽车大师这个赛题把场景限制得很具体车主在平台上提问维修技师给出一长段回答参赛者要训练模型把这组问答压缩成一条有结论、可推理的摘要。换句话说输入是“问题 回答”两段长文本输出是一句短摘要而摘要里的关键信息还要能被后续的推理环节直接用起来。这条技术路线最适合两类人刚接触生成式 NLP、想用 seq2seq 练手的工程师以及要在真实问答场景里做信息压缩的从业者。下面我会按数据准备、seq2seq 基线、注意力机制、训练调参和推理验证的顺序把这一整套方案拆开讲清楚。2. 数据准备先做好问答对如何拼装、清洗和变短2.1 这个任务和普通摘要不一样输入是“问题回答”的双段文本如果做过新闻摘要你会发现新闻标题和正文的语义是高度对齐的直接抽第一段往往就能拿个不错的 ROUGE。汽车大师这类问答摘要完全不是这个逻辑问题里带着车型、年款、故障现象回答里带着分析过程、诊断结论和维修建议而参考摘要通常是把“结论”和“依据”融合成一句通顺的话。比如问题可能是“13 款朗逸1.6L 自动挡冷车启动时发动机舱吱吱响热车后消失”回答会分析皮带老化、张紧轮磨损、水泵轴承等好几个可能原因摘要却往往只写“冷车启动异响多为发电机皮带或张紧轮老化建议更换皮带及张紧轮”。这个合成过程既需要从回答里定位结论又需要回扣问题里的车型和症状约束所以普通的抽取式摘要很难覆盖生成式模型才是主流做法。常见的数据格式是 JSON Lines每行一条样本。核心字段就三个question、answer、summary分别对应用户提问、技师回答和参考摘要。也有些版本会把问题拆成 title 和 content或者额外给一段“推理依据”。拿到手先不要急着训模型把每一个字段的真实长度分布打印出来看看回答动辄几百字摘要往往只有几十个字输入输出长度差异极大这直接决定了后续的词表构建、填充策略和截断策略应该怎么做。2.2 JSON 解析与清洗把原始问答转成训练三元组数据清洗的核心目标是把原始 JSON 转成干净的三元组问题、回答、摘要。我一般先把问题截断到一个合理长度再把回答单独截断最后才做拼接。这样能避免“问题太长把回答挤掉”这种低级问题。下面这段解析脚本在多个类似赛题里都能直接用import json import re def tokenize(text): # 中文按汉字切连续的英文/数字串作为一个整体标点单独成词 return re.findall(r[\u4e00-\u9fffA-Za-z0-9]|[^\s\u4e00-\u9fffA-Za-z0-9], text) def clean_text(text): text text.replace(\u3000, ).replace(\n, ).strip() text re.sub(r\s, , text) return text def load_samples(data_path, max_q_len80, max_a_len200): samples [] with open(data_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue obj json.loads(line) question clean_text(obj.get(question, ))[:max_q_len] answer clean_text(obj.get(answer, ))[:max_a_len] summary clean_text(obj.get(summary, )) if not question or not answer or not summary: continue src question [SEP] answer samples.append({src: src, tgt: summary}) return samples这里的 max_q_len 和 max_a_len 不是随便拍的。我一般先把训练集里 question 和 answer 的长度分布跑一遍question 超过 80 个字符的样本占比很低而 answer 超过 200 个字符的样本非常多但真正参与结论表达的往往是前半段。如果你把 answer 全量保留词表里会出现大量低频专业词模型训起来很慢且容易过拟合。代码里 clean_text 做了两个重要处理把全角空格和换行统一成半角空格再把连续空白压缩成单个空格。这一步不做后续 tokenize 会把换行符当成独立词词表里多出一堆没意义的词条。清洗完建议顺手统计一下过滤后还剩多少条样本、摘要的平均长度、src 的平均长度。摘要太长的样本比如超过 60 个字可以在训练时直接截断或者剔除因为这种“长摘要”很多是从多个回答片段拼出来的参考质量本身就不稳定。2.3 词表构建与 mini-batchOOV 和截断怎么处理seq2seq 模型离不开固定词表。常见的做法是按词频过滤低频词把出现次数少于某个阈值的词替换成 UNK。min_freq2 是个比较稳的起点词频为 1 的词通常是车型名、人名或者拼写变体这些词对摘要生成贡献不大却把词表撑得很大。词表里四个特殊符的顺序建议固定后续代码里到处要用from collections import Counter PAD, SOS, EOS, UNK 0, 1, 2, 3 SPECIAL_TOKENS [pad, sos, eos, unk] def build_vocab(samples, min_freq2): counter Counter() for s in samples: for tok in tokenize(s[src]): counter[tok] 1 for tok in tokenize(s[tgt]): counter[tok] 1 vocab {tok: idx for idx, tok in enumerate(SPECIAL_TOKENS)} for tok, freq in counter.most_common(): if freq min_freq and tok not in vocab: vocab[tok] len(vocab) return vocab这里把 src 和 tgt 的词合并统计词频好处是摘要里的高频词也能进入词表。坏处是 src 里的大量无关背景词会挤占词表容量。实际操作中可以用双词表encoder 用 src 统计出的词表decoder 用 tgt 统计出的词表。对于初学者单词表更省事但我建议至少把 min_freq 从 1 提到 2否则词表大小会从一两万跳到四五万Embedding 层和输出层的参数数量都跟着翻倍。mini-batch 的组装是另一个容易翻车的地方。PyTorch 的pack_padded_sequence要求序列按长度降序排列所以 collate_fn 里除了 padding 和记录真实长度还必须做排序import torch from torch.nn.utils.rnn import pad_sequence def collate_fn(batch, vocab, max_src_len200, max_tgt_len64): src_ids_list, tgt_ids_list [], [] for item in batch: src_tokens tokenize(item[src])[:max_src_len] tgt_tokens tokenize(item[tgt])[:max_tgt_len] src_ids [vocab.get(t, UNK) for t in src_tokens] tgt_ids [SOS] [vocab.get(t, UNK) for t in tgt_tokens] [EOS] src_ids_list.append(torch.tensor(src_ids, dtypetorch.long)) tgt_ids_list.append(torch.tensor(tgt_ids, dtypetorch.long)) src_ids pad_sequence(src_ids_list, batch_firstTrue, padding_valuePAD) tgt_ids pad_sequence(tgt_ids_list, batch_firstTrue, padding_valuePAD) src_lens torch.tensor([len(x) for x in src_ids_list], dtypetorch.long) # pack_padded_sequence 要求降序 src_lens, order torch.sort(src_lens, descendingTrue) src_ids src_ids[order] tgt_ids tgt_ids[order] return src_ids, src_lens, tgt_idsmax_tgt_len64 是因为参考摘要很少超过 60 个词。这里有几个参数初学者容易调错SOS 和 EOS 都要拼进去但 padding 值必须用 PAD 而不是 0 以外的东西padding 后 tgt 里会出现大量 PAD训练时计算损失要把 ignore_index 设为 PAD排序后 tgt_ids 必须跟着 src_ids 一起重排否则 batch 内部的配对就乱了。很多人第一次跑通后 loss 乱跳回头查基本都是这三处的问题。3. 跑通 seq2seq 基线双向 GRU 编码器 解码器的最小训练回路3.1 为什么基线先选 GRU参数少、收敛快适合比赛试错比赛场景下基线模型的选择标准不是“效果最好”而是“快速跑通、快速找到 bug”。LSTM 和 GRU 在这个任务上的效果差距很小但 GRU 的参数只有 LSTM 的四分之三训练速度和显存占用都更友好。汽车大师问答摘要这种中等规模数据集GRU 在十几个小时内就能收敛到能看到效果的 checkpointLSTM 往往要多跑 30% 的时间。所以我的建议很直接基线用 GRU如果后续要刷分再换 LSTM 或者 Transformer。编码器用双向 GRU解码器用单向 GRU这是 seq2seq 摘要任务最常见的配置。双向编码器能让每个时间步的隐状态同时看到前文和后文对“皮带老化”这种跨词组的语义非常有用。双向带来的问题是如何把两个方向的最终状态合并成解码器的初始状态常见做法是把 forward 方向和 backward 方向的最后一个隐状态拼接过一个线性层压缩到单向隐状态维度再用 tanh 激活。下面这段 Encoder 实现可以直接抄import torch import torch.nn as nn import torch.nn.functional as F class Encoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, dropout0.1): super().__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idxPAD) self.gru nn.GRU(embed_size, hidden_size, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(hidden_size * 2, hidden_size) self.dropout nn.Dropout(dropout) def forward(self, src_ids, src_lens): emb self.dropout(self.embedding(src_ids)) packed nn.utils.rnn.pack_padded_sequence( emb, src_lens.cpu(), batch_firstTrue, enforce_sortedTrue ) packed_out, hidden self.gru(packed) encoder_outputs, _ nn.utils.rnn.pad_packed_sequence( packed_out, batch_firstTrue ) # hidden: (2, B, H)分别对应 forward 和 backward 最后一个时刻 h_fwd, h_bwd hidden[0], hidden[1] hidden_cat torch.cat([h_fwd, h_bwd], dim-1) # B, 2H decoder_init torch.tanh(self.fc(hidden_cat)) return encoder_outputs, decoder_init这段代码里最容易出错的是 hidden 的取值。PyTorch 的 GRU 返回的 hidden 维度是 (num_layers * num_directions, batch, hidden_size)。单层双向时hidden[0] 是 forward 方向的最终状态hidden[1] 是 backward 方向的最终状态。很多同学直接取 hidden[-1] 当成一个方向的输出但在双向模型里 hidden[-1] 是 backward 的最终状态取错了方向整个初始状态就废了。参数上embed_size256、hidden_size256 是一个性价比很高的起点如果显存紧张把 hidden_size 降到 128 也能跑但摘要质量会明显下降。3.2 Decoder 与训练循环teacher forcing 的时机和梯度裁剪没有 attention 的 baseline Decoder 很简单每个时间步把上一时刻的 token 输入到 GRU用当前隐状态预测下一个 token。上一节已经是完整代码了这里给出 Decoder 和训练循环中最关键的train_step。训练时我不会一次性把整个序列解出来而是逐时间步循环这样能用 teacher forcing 控制模型看到真实历史的比例import random def train_step(model, src_ids, src_lens, tgt_ids, teacher_forcing_ratio0.5): # src_ids: B,T_src tgt_ids: B,T_tgt encoder_outputs, decoder_init model.encoder(src_ids, src_lens) batch_size src_ids.size(0) decoder_input tgt_ids[:, 0].unsqueeze(1) # 强制从 sos 开始 hidden decoder_init.unsqueeze(0) # 1,B,H loss 0.0 for t in range(1, tgt_ids.size(1)): logits, hidden model.decoder(decoder_input, hidden) # B,1,V loss F.cross_entropy( logits.squeeze(1), tgt_ids[:, t], ignore_indexPAD ) use_teacher random.random() teacher_forcing_ratio if use_teacher: decoder_input tgt_ids[:, t].unsqueeze(1) else: decoder_input logits.argmax(dim-1) # 模型自己猜 return loss / (tgt_ids.size(1) - 1)teacher_forcing_ratio0.5 的意思是训练时每个时间步有 50% 概率把真实 token 喂回解码器另外 50% 概率用模型上一时刻的预测作为输入。这个比例不能太高太高会让模型在推理阶段一遇到自己的错误预测就连环出错也不能太低太低收敛会很慢。我一般从 0.5 开始训练到后半程降到 0.3。训练主循环里还缺一个关键操作梯度裁剪。GRU 在长序列上很容易梯度爆炸loss 突然变成 NaN 的案例几乎都是没做裁剪optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): for batch in dataloader: optimizer.zero_grad() src_ids, src_lens, tgt_ids [x.to(device) for x in batch] loss train_step(model, src_ids, src_lens, tgt_ids) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step()Adam 的初始学习率 1e-3 在这个任务上是安全的clip 值 5.0 是我的个人偏好太小会让训练变慢太大会失去保护作用。这里要提醒一句loss 在下降不代表生成质量在变好seq2seq 的 loss 是逐 token 的交叉熵模型可能学会了复制高频词但没学会组织句子。我一般在每 500 步做一次 greedy 解码把预测的摘要打印出来和参考摘要对比靠肉眼判断模型是不是真的在“变聪明”。4. 给解码器装上 attention上下文向量的对齐逻辑与 PyTorch 实现4.1 摘要任务的信息瓶颈固定向量扛不住长回答纯 seq2seq 的问题在问答摘要任务里暴露得很明显解码器每一步都只能依赖编码器最后时刻压缩出的一个固定向量。如果车主描述有 300 个字技师回答有 200 个字这个固定向量要承载全部信息早期输入的内容早就被后续 token 冲刷掉了。而汽车大师摘要偏偏需要跨段对齐“冷车启动异响”在问题的开头“皮带老化”在回答的中后段模型要建立这两个位置之间的关联。没有 attention解码器只能“记住个大概”生成出来的摘要经常张冠李戴。attention 的核心思想是解码器在第 i 步生成词之前先计算当前隐状态与编码器每个位置隐状态的相似度把相似度归一化成权重再对编码器所有隐状态做加权求和得到这一步专属的上下文向量。这个机制最早由 Bahdanau 提出也是标题里 seq2seq_attention 对应的一类实现相当于给解码器装了一个“通用注意力模块”PyTorch 里完全可以自己写成一个独立的 nn.Module 复用。4.2 Bahdanau attention 的 PyTorch 实现mask 是关键Bahdanau 注意力又叫加性注意力它对编码器输出和当前解码器隐状态各做一次线性变换再相加经过 tanh 和另一个线性层得到标量分数。公式是e_ij v^T tanh(W_h h_j W_s s_{i-1})a_ij softmax(e_ij)c_i Σ_j a_ij h_j代码实现时要注意一个很容易被忽略的细节padding 位置不能参与注意力打分。编码器输出经过 pad_packed_sequence 后所有短句子的尾部都是 PAD 对应的隐状态如果不把这些位置的分数压成负无穷模型会学到“注意力放到 PAD 上也无所谓”训练 loss 照样降但注意力热图完全失去可解释性。下面是完整实现class BahdanauAttention(nn.Module): def __init__(self, hidden_size): super().__init__() self.W_h nn.Linear(hidden_size * 2, hidden_size) self.W_s nn.Linear(hidden_size, hidden_size) self.v nn.Linear(hidden_size, 1) def forward(self, decoder_hidden, encoder_outputs, mask): # decoder_hidden: B, H - B, 1, H q self.W_s(decoder_hidden).unsqueeze(1) # encoder_outputs: B, T, 2H - B, T, H k self.W_h(encoder_outputs) scores self.v(torch.tanh(q k)).squeeze(2) # B, T scores scores.masked_fill(mask 0, -1e9) # 屏蔽 padding attn_weights F.softmax(scores, dim-1) context torch.bmm(attn_weights.unsqueeze(1), encoder_outputs).squeeze(1) return context, attn_weightsmask 的来源是src_ids ! PADshape 为 B, T。把 mask 传进 attention 之前要确保它和 encoder_outputs 的 T 维度一致因为 pad_packed_sequence 返回的序列长度是整个 batch 里最长的那条所以 mask 直接用src_ids ! PAD生成即可。这里的 hidden_size 是解码器的隐状态维度encoder_outputs 的最后一维是 hidden_size * 2所以 W_h 输入维度是 2HW_s 输入维度是 H。我见过有人把这两个线性层搞反或者都写成 H结果 score 计算的维度对不上报错后只能靠猜。先把这个对应关系写清楚抄代码的时候就不会乱。4.3 Decoder 接入 context 后的结构变化加了 attention 之后Decoder 的每个时间步不再只吃 token embedding而是把上一时刻的上下文向量和当前 token 的 embedding 拼接在一起再送进 GRU。这一步的目的是让 GRU 在生成每个词时都能“看着”原文本的相关位置。下面是完整 Decoderclass Decoder(nn.Module): def __init__(self, vocab_size, embed_size, hidden_size, dropout0.1): super().__init__() self.embedding nn.Embedding(vocab_size, embed_size, padding_idxPAD) # 输入维度 embedding encoder_outputs 维度2H self.gru nn.GRU(embed_size hidden_size * 2, hidden_size, batch_firstTrue) self.attention BahdanauAttention(hidden_size) self.fc_out nn.Linear(hidden_size, vocab_size) self.dropout nn.Dropout(dropout) def forward(self, decoder_input, hidden, encoder_outputs, mask): emb self.dropout(self.embedding(decoder_input)) # B,1,E context, attn_weights self.attention( hidden.squeeze(0), encoder_outputs, mask ) # context: B,2H context context.unsqueeze(1) # B,1,2H rnn_input torch.cat([emb, context], dim-1) # B,1,E2H out, hidden self.gru(rnn_input, hidden) # out: B,1,H logits self.fc_out(out) # B,1,V return logits, hidden, attn_weights这里有一个维度细节attention 返回的 context 是 B,2H和 embedding 拼接后 GRU 输入维度变成 embed_size 2H。GRU 输出维度是 H最后接一个 Linear(H, V) 预测词表分布。hidden 在 GRU 之间传递时要保持 (1,B,H) 的形状但 attention 需要的是 (B,H)所以传入 attention 前要 squeeze拿到新 hidden 后继续做循环。如果你用的是多层解码器这里要额外处理每一层的 hidden初学者建议先用单层跑通再加深。训练循环大部分逻辑和上一章一致只有一个小改动train_step里每步都要把 encoder_outputs 和 mask 传进 decoder同时接收 attention 权重用于可视化。我习惯把 attention 权重存下来每跑完一个 epoch 挑几个样本画热图看模型是否关注到了“车型型号”和“故障结论”这些位置这一步对判断是模型问题还是数据问题非常有效。5. 训练与调参常见坑seq2seq 摘要最容易翻车的四处细节5.1 损失降但输出全是重复词先查 SOS/EOS 和 teacher forcing现象训练集上 loss 一路下降但验证时生成的摘要里全是“皮带 皮带 皮带”“异响 异响 异响”这种重复词。原因分两类。第一目标序列构造时没有把 EOS 加在末尾或者加了但计算损失时没有对最后一个 EOS 做预测导致模型永远学不到“这句话该结束了”于是只能一直重复上一个高频词。第二teacher forcing 比例设得太高比如 1.0 或 0.9模型从没见过自己的错误输出推理时一旦第一步选了个低频词后面就全乱了。解决检查 tgt_ids 是否[SOS] tokens [EOS]t 从 1 循环到 tgt_ids.size(1) - 1确保每个 token 都会被预测一遍训练初期 teacher_forcing_ratio 维持 0.5如果还重复降到 0.3 并增大 dropout。5.2 解码停不下来EOS 被 beam search 当成了普通候选现象训练正常、验证 loss 正常但 greedy 解码时序列都生成了 80 个 token 还在继续完全没有 EOS 的迹象。原因有两个层面一是 EOS 在词表里的初始分数比较低训练样本里 EOS 只会出现在句尾模型天然有“多写几个词”的偏向二是推理时如果用了 beam searchbeam 会把 EOS 当成一个普通候选词它的累计分数不如普通词高于是被其他候选挤掉。解决训练时在 loss 上对 EOS 的 token 加权比如把 EOS 的权重设成 2.0强迫模型重视结束标志推理时对 beam search 加长度惩罚对包含 EOS 的候选做奖励或者当某条 beam 生成 EOS 后直接冻结它不再扩展。更省事的办法是解码长度超过 max_len 时强制截断但这只能止血不能解决模型本身不学 EOS 的问题。5.3 ROUGE 死活不涨多半是输入被截断把关键信息切掉了现象换 attention、调 lr、加 dropoutROUGE-L 始终停留在 0.3 左右上不去。我排查这类问题会先看一条具体样本的输入输出。如果发现参考摘要里的“冷车启动”“发电机皮带”在 src 里根本找不到那问题不在模型在数据预处理。前面 2.2 节我特意把 answer 单独截断到 200 字符就是因为有些同学直接把 question answer 拼起来再截断遇到 question 本身很长的情况answer 会被砍掉一大半结论信息全丢了。解决把截断策略改成“question 截断到 80、answer 截断到 200再拼接”并在预处理后写一个简单脚本统计参考摘要里的 bigram 有多少比例出现在截断后的 src 里。这个覆盖率如果低于 80%说明截断太狠需要放大 max_a_len。很多调参调不动的“玄学”最后查出来都是数据预处理的问题。5.4 注意力热图乱成一团padding 位置也在打分现象attention 可视化画出来每一行的权重都散布在整个宽度上甚至句尾的 PAD 位置权重很高完全看不出对齐关系。原因很明确初始化 mask 时用的是src_ids ! PAD但 encoder_outputs 是从pad_packed_sequence恢复的它的长度等于 batch 内最长序列。如果你的 mask 在预处理时和 src_ids 一起被排序重排过顺序应该没问题但如果 mask 是用原始 batch 生成的而 encoder_outputs 是重排后的顺序两者就错位了。解决mask 必须在 collate_fn 里随着 src_ids 一起重排或者在 Encoder forward 里根据传入的 src_lens 重新生成 mask。我习惯直接在 forward 里生成mask (src_ids ! PAD).unsqueeze(1) # B,1,T 用于广播另外attention 的 dropout 也很重要。没做 attention dropout 时模型容易极度依赖单个位置的权重热图看起来就是一根“细线”或者一片“乱麻”。我一般单独给 attention 的 score 加一个 dropout0.1能让热图更平滑也会小幅提升 ROUGE。5.5 训练太慢pack 没有按长度降序导致大量无效计算现象GPU 利用率很低一个 epoch 要跑很久。原因如果没用 pack_padded_sequence每个 batch 都会按最长序列做 paddingbatch 里短样本占比高时浪费严重或者用了 pack 但 enforce_sortedFalsePyTorch 内部要重新排序额外开销也不小。解决在 collate_fn 里完成降序排序让pack_padded_sequence走 enforce_sortedTrue 的快路径。另一个常见慢点是词表太大输出层 Linear(H, V) 的计算量随 V 线性增长可以把词表的大小压缩到 2 万以内或者用 weight tying 让 embedding 和输出层共享权重。前者简单有效后者省显存但实现稍复杂建议优先控制词表。6. 推理验证与进阶用 beam search 和 ROUGE 替代肉眼抽查6.1 beam search 的 k 怎么选训练完成后greedy 解码只能作为错误排查工具真正用来评估和交付应该用 beam search。k3 是小规模摘要任务的稳妥起点比 greedy 少很多重复词又比 k5 快不少。k5 在这种中文短摘要上收益递减而且容易生成“过于顺滑”但偏离原意的句子。beam search 实现时记得在每个候选维护自己的 decoder hidden state不能所有候选共用一份。长度归一化建议用“累计 log 概率除以已生成步数的 0.7 次方”这个超参数比 k 本身更影响质量k3 配 length_penalty0.7 是我常用的组合。6.2 ROUGE 评估脚本和注意力可视化评估摘要任务ROUGE 比 BLEU 更贴合语义重叠度因为 ROUGE 衡量的是参考摘要里的 n-gram 有多少被预测出来了BLEU 更偏向机器翻译的流畅度。用rouge-score库三行就能跑from rouge_score import rouge_scorer scorer rouge_scorer.RougeScorer([rouge1, rouge2, rougeL], use_stemmerTrue) scores scorer.score(reference_summary, predicted_summary) print(scores[rougeL].fmeasure)注意里的四个指标分开看ROUGE-1 反映关键词覆盖ROUGE-2 反映短语层面的连贯性ROUGE-L 反映最长公共子序列和语序。我评估时会额外加一条“是否包含车型关键数字”的硬规则比如摘要里“1.6L”“13 款”这类带数字的词组比单纯看 ROUGE 更能体现推理能力。注意力可视化则用 matplotlib 画 heatmap横轴是输入 token纵轴是输出 token颜色越深表示权重越高。汽车大师摘要里如果模型生成“皮带”时对输入中“皮带老化”位置的权重很深说明 attention 确实在发挥作用。最后说一点教训我早期跑这类 seq2seq 摘要赛题时总把评估脚本放到最后一步结果是每次训练完才发现 beam search 忘写了长度惩罚、ROUGE 统计的参考文件没对齐白白浪费好几个小时。后来我把评估脚本和训练脚本写在同一份代码里每两个 epoch 自动跑一次验证集 ROUGE-L把“肉眼抽查”变成“指标监控”模型好坏一眼就能判断。这个习惯帮我在后续多个生成任务里少踩了很多坑也希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/6 14:44:18

AI Native研发范式落地手册:从团队组建到全流程重构

最近半年我带团队做了一轮比较彻底的“AI Native”改造, 不是某个环节引入个AI工具,而是把整个研发范式推翻重来 。很多朋友看了都问,你们到底怎么弄的?我想把这套落地过程整理出来,包含思路、工具链、团队分工、踩坑…

2026/10/6 14:44:18

WinCC flexible 2008 SP4下载SMART 700 IE实操指南

1. 项目概述:为什么这个操作值得你花45分钟认真读完 WinCC flexible 2008 SP4 给西门子 SMART 700 IE 下载程序,表面看只是“点几下鼠标”的事,但实际现场里,超过六成的工程师卡在第一步——网线插上却找不到设备。我去年在东莞一…

2026/10/6 14:39:17

惠普SFF小主机算力升级实战:插上Tesla P4和Intel DG1

前阵子清理手头的旧配件,翻出两台惠普SFF小主机,一台是HP EliteDesk 800 G4,带i5-8500和16G内存,另一台是HP ProDesk 400 G7,带i3-10100和16G内存。原计划是继续当软路由和下载机用,但看着PCIe x16插槽空着…

2026/10/6 15:39:24

VLA模型实战:π0驱动Aubo机械臂完成抓取部署全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 15:39:24

RK3588 NPU加速DeepSeek蒸馏模型部署:从模型转换到实测对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 15:39:24

ADS1220与PT100/PT1000高精度温度采集方案:从原理到0.01℃实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 15:34:24

PCIe配置空间与BAR空间详解:从枚举到FPGA实战调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/6 4:01:51

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/5 17:38:27

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 0:03:23

MR25H40CDF+STM32F031C6工业级高可靠数据存储方案

1. 项目概述:为什么在工业现场非得用 MR25H40CDF 配 STM32F031C6 做数据存储?在工厂产线的 PLC 控制柜里、在风电变流器的散热片背面、在矿井监测终端的金属外壳下,你经常能看到一块指甲盖大小的黑色芯片——它既不是 Flash,也不是…

2026/10/6 0:03:23

MRAM+STM32工业断电数据保全实战指南

1. 项目概述:为什么在工业现场非得用 MR25H40CDF 配 STM32F031C6 做数据存储?在工厂产线的PLC柜里、在野外无人值守的环境监测终端里、在高速运转的包装机控制板上,你经常能看到一块指甲盖大小的黑色芯片,旁边贴着“MR25H40CDF”丝…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑