TextRank+Seq2Seq+PyQt5:中文摘要与关键词辅助生成系统实战
简介基于TextRank与Seq2Seq算法的文章摘要、标题与关键词辅助生成系统面向自然语言处理学习者及科研人员解决长篇幅文本信息提取与可视化交互问题。工程涵盖数据预处理、抽取摘要、模型搭建与编译、训练保存、PyQt5界面开发及应用封装六大模块配套学术论文与维基百科等来源的训练数据集整体已封装为可在PC端直接运行的可执行程序。压缩包共1972个文件约324.87MB以Python源码、pyd动态库、dll依赖库、ui与qrc界面配置文件、训练数据csv及日志文本为主目录结构清晰便于定位关键模块。已有252人浏览学习。资源提供完整可复现的工程代码与数据集适合需要系统学习TextRank抽取、Seq2Seq生成、PyQt5交互设计及模型部署全流程的开发者参考支持在此基础上二次扩展与调优。1. TextRankSeq2SeqPyqt5 辅助生成系统的定位它到底解决了谁的痛点TextRankSeq2SeqPyqt5 这个组合第一眼看上去像是把两个摘要方案硬绑在一起实际上它是当前做文章辅助生成最省钱的配比TextRank 不消耗训练数据先把原文里信息量最大的句子捞出来Seq2Seq 再把捞出来的内容改写成通顺的生成式摘要最后通过 PyQt5 收进一个桌面工具里。这个系统解决的是单模型的尴尬——只跑 TextRank输出永远是原文句子句式死板只跑 Seq2Seq数据不够时生成的摘要全是不通顺的废话。适合做课程设计、横向项目演示或者想给内容编辑搭一个辅助工具的人。整个工程牵扯算法、训练数据、界面三块这也是这类项目一直是毕设和工程复现热门的原因算法部分能讲清楚图模型和注意力机制训练部分能体现数据工程界面部分又能直接演示。下文从选型依据、数据集准备、工程落地到踩坑按一条可复现的路径展开。每一条参数和代码都是我实际跑过之后留下的版本你照着改能少走不少弯路。2. 选型逻辑先立住TextRank 用图排序捞重点Seq2Seq 用 attention 做改写很多新手上来就纠结要不要换成 BERT 或 T5我的建议是先把这两条经典路数吃透。TextRank 和 Seq2Seq 不但在原理上互补实现上也是从易到难的递进前者半小时能出结果后者需要你认真准备数据。这篇的选型逻辑本质是回答“为什么一个 2014 年的图算法和一个 2015 年的生成模型到现在还能稳定地撑起一个辅助生成工具”。2.1 TextRank用邻接矩阵把文章变成图迭代算词的重要度TextRank 不是深度模型不需要训练环节所以它对数据集和硬件零要求。它把一篇文章的每个候选词当成图的一个节点窗口内同时出现的两个词之间连一条带权重的边。这个步骤等价于你给所有候选词构建了一个邻接矩阵矩阵里的数值是两个词在滑动窗口内共现的次数。有了图之后再用类似 PageRank 的迭代公式给每个节点打分分数越高这个词就越可能是文章的关键词。打分公式的核心是每个节点的得分由指向它的邻居的得分按权重传递过来同时加一个阻尼系数 d默认取 0.85。数学形式是S(Vi) (1 - d) d × Σ( Wji × S(Vj) / Σk(Wjk) )这个公式里最容易被忽略的是分母——邻居 j 的权重要除以它所有出边的权重之和用 python 构建邻接矩阵时如果只统计入度不按列归一化最后的得分就会偏向那些窗口内出现特别频繁的通用词。我一般会把矩阵按列归一化迭代时一直更新到两次得分差小于 1e-4 为止max_iter 不需要设太大200 次以内基本都能收敛。TextRank 和 TF-IDF 的区别在于 TF-IDF 只看全局统计而 TextRank 看的是词在局部窗口里的共现结构。一个词如果反复出现在不同句子的核心位置它的图得分就会明显高于那些只在一两个句子里扎堆的词。这也是它做关键词抽取时对“中心词”的捕捉比 TF-IDF 更自然的原因。以下是这套算法最常见的初始参数后续可以按语料调整参数常见值作用window5滑动窗口大小决定词的共现范围d0.85阻尼系数控制邻居信息的传递比例max_iter200迭代上限防止不收敛死循环min_diff1e-4两次迭代得分差小于该值即停止2.2 Seq2Seqencoder-decoder 加 attention生成式摘要的主力架构Seq2Seq 的核心思路是把原文编码成一个序列的隐状态再用 decoder 逐字生成摘要。原文进入 encoder 后每个时间步都会产出一个隐状态这整串隐状态就相当于模型对文章的“记忆”。decoder 每生成一个字需要从这份记忆里选取最相关的部分这个选取动作就是 attention 模块。decoder 里的注意力模块最常用的是 generic attention如果你去搜 seq2seq pytorch 教程见到最多的公式是score v^T × tanh(W_h × h_enc W_s × h_dec)其中 h_enc 是 encoder 所有时间步的隐状态h_dec 是 decoder 当前步的隐状态。两者分别过一层线性映射后相加经过 tanh 和向量 v 点积得到当前步对每个 encoder 位置的重要度分数。这个分数再走 softmax 变成权重最终把 encoder 隐含层状态按权重加权求和得到一个 context 向量拼进 decoder 的输入。没有 attention 的 Seq2Seq 在长文本摘要任务上几乎不可用因为 encoder 最后一刻的隐状态根本装不下整篇信息。有了 attentiondecoder 每一步都能“回头翻原文”生成的摘要才可能落到原文重点上。训练时还要用 teacher forcing也就是按一定概率把真实的上一时刻 token 喂给 decoder 而不是让模型吃自己刚生成的 token这个概率一般取 0.5 到 0.8太低训练不稳太高推理时会因为没见过自己的错误输出而崩。这个工程里还可以在 decoder 末尾加一个多标签输出头同时对关键词做预测。这样标题里“关键词辅助生成”就不是把 TextRank 结果和 Seq2Seq 结果简单拼界面而是让两者共享 encoder 的语义表示训练阶段互相拉一把。2.3 抽取式与生成式没有谁取代谁分工与兜底TextRank 是抽取式Seq2Seq 是生成式工程上最常见的错误是把它们当成竞品去二选一。两者的失败模式完全不同TextRank 的摘要永远来自原文忠程度高但句式单一信息密度低Seq2Seq 的摘要可以改写和压缩但训练不足时容易生成不通顺甚至编造的内容。对比如下维度TextRank 抽取式Seq2Seq 生成式数据依赖不需要训练数据需要几千到几万条配对语料原文忠实度全部来自原文可能改写或丢失信息句式保持原句形态可以重新组织可解释性能指出选了哪些句子黑匣子难定位错误来源失败模式重点抓偏但不产生新错误重复词、语法不通、主题跑偏所以我一般建议系统的最终管线做成混合式先让 TextRank 从长文里捞关键句拼成一个压缩文本再把压缩文本喂给 Seq2Seq 做生成式改写。这样 TextRank 相当于给 Seq2Seq 圈定了主题范围attention 的压力小一半生成质量会明显好于直接把整篇长文档丢给模型。这也是标题里三个组件能放在同一个工程里的根本原因。2.4 跑通顺序先有基线召回再上生成模型落地这个系统不要一上来就训 Seq2Seq我的习惯是分四步走。第一步准备好分词和停用词处理模块按句切分文档第二步跑通 TextRank先把抽取式摘要和关键词当成系统的 v1 版本这一步能让你立刻有一个可演示的界面第三步把 TextRank 选出的关键句拼成压缩文本作为后续 Seq2Seq 训练和推理的输入第四步再训 Seq2Seq并把两套结果在界面里对比展示。这个顺序的好处是每一步都有可验证的产出。TextRank 的产出可以直接用肉眼判断“重点抓得对不对”如果这一步就不靠谱后面 Seq2Seq 再强也会被带偏。而且 TextRank 不需要训练你可以先把 GUI 和接口形态定下来再回头折腾数据不会一上来就卡在训练环境上。3. 训练数据集按“训练自己的数据集”的标准准备摘要语料Seq2Seq 的质量上限基本由数据集决定模型结构只是把数据里的规律拟合出来。很多人在这个标题上翻车不是代码不对而是数据太脏、太少或者格式不统一。这一章就是解决“训练自己的数据集”这件事——像准备目标检测数据集一样把摘要数据的格式、清洗、切分全部定义清楚。3.1 摘要数据的“标注格式”源文本、参考摘要、关键词三元组做目标检测时yolov5、yolov8 都要求 txt 标注与图片同名摘要系统也需要自己的统一格式。我一般推荐用 JSONL每行一个样本包含原文、标题、摘要和关键词四个字段。{content: 当地时间19日某某公司发布新品市场反应强烈股价单日上涨超过8%分析师认为..., title: 某某公司发布新品 股价大涨, summary: 公司发布新品带动股价上涨分析机构看好后续表现。, keywords: [新品发布, 股价, 分析师]}来源上常见做法是爬取公开新闻网页或者直接用已有的公开中文新闻语料。清洗时先用正则去掉 HTML 标签和空白字符再筛掉长度异常的样本。我一般会保留原文 150 到 800 字、摘要 30 到 100 字的样本这个范围能避开“标题即摘要”的偷懒数据也能保证 seq2seq 有足够的压缩空间。去重这步要仔细用“筛选一样的重复项”按 title 或者 content 的 md5 做一遍重复样本混在训练集和验证集会直接拉高评估指标让你误以为模型很能打。这里要提醒一点从公开网络抓取的数据要注意授权问题商业项目必须用明确授权的语料个人学习用也要注意来源合规。这是做数据工程绕不开的底线。3.2 没有人工摘要怎么办用 TextRank 生成弱标签先让模型“有得学”现实情况是你能轻松拿到几百篇文章但拿不到对应的参考摘要。这时候可以用标题里的 TextRank 反向造一批弱标签。步骤是对每篇正文跑 TextRank得到按重要度排序的句子取前 3 句拼成一个伪摘要再和该新闻的标题算一下 ROUGE-1如果重合度超过 0.5 就认为这个伪摘要靠谱保留下来当训练数据否则丢弃。这个做法有两个好处。第一它把无监督的 TextRank 变成了标注工具你能在半小时内从原始语料里筛出几千条质量还过得去的训练样本第二弱标签能让 Seq2Seq 先学会“句子重组”的能力等模型已经能把句子连接得通顺了再混入人工高质量摘要做精调。整个思路和半监督学习是一样的先在大规模弱标签上预训练再在小规模强标签上精调效果通常远好于拿几千条人工数据硬训。3.3 预处理三件套截断、padding、mask决定模型能不能收敛预处理直接决定训练能不能收敛。首先要确定输入序列的最大长度我一般设 max_enc_len400max_dec_len80。超长文章按头尾截断不友好常见做法是保留开头几个段落因为新闻类文章的开头往往已经包含了核心信息。处理时可以用 python 数组切片命令直接截简单高效。分词层面用 jieba 分词后构建词表控制 min_count5 可以过滤掉出现次数太少的生僻词避免词表爆炸。未登录词统一映射为 [UNK]推理时遇到没见过的词不会直接崩。padding 和 mask 是最容易出错的一步。因为一个 batch 内序列长度不同短序列要补 0 到 batch 最大长度但如果 loss 计算时把 padding 位置也算进去模型会学会一个很阴间的技巧在 padding 位置预测结果为 0 就能降低 loss于是 decoder 开始输出一堆空洞。解决方法是构造一个 mask 矩阵loss 计算时对 padding 位置置零。TextRank 构建邻接矩阵时的初始化也常被忽略全零矩阵初始化后要记得只对窗口内共现位置赋值否则图上全是自环排序结果会失真。3.4 切分、缓存与避免数据泄漏数据集切分采用 8:1:1 划分训练、验证、测试三部分但 shuffle 时要固定随机种子保证每次训练实验用的是同一份划分不然没法对比模型调参前后的效果差异。这里容易被忽略的是数据泄漏同一新闻的不同版本、同源转载文章只能出现在同一个划分里。TextRank 是图模型倒无所谓泄漏但 Seq2Seq 是有监督训练测试集里混进训练数据会让评估结果虚高让你误以为模型已经很强。切分完成后最好把分词、截断、padding 之后的张量一次性缓存成 pkl 文件。这样每次训练不用重新跑一遍 jieba 分词和字符串处理能省下大量时间。缓存文件路径可以在训练脚本里用参数传入给数据切分做个缓存算是给自己留的后悔药重新跑数据时不用再等五分钟。4. 工程落地依赖、TextRank 实现、Seq2Seq 训练和 PyQt5 界面的完整跑通这一章是整套工程的核心我把目录结构和三块关键代码都按可复现的方式写在下面。目录结构按常见 python 工程规范组织每个模块单独放一个文件便于单独调试也便于后面接入新模型。4.1 依赖与目录requirements.txt 一把梭pyqt5 安装有讲究工程目录我一般拆成四块数据预处理放 data_processTextRank 和 Seq2Seq 放 models界面放 gui训练入口放根目录的 train.py。结构如下article_tool/ ├─ requirements.txt ├─ train.py # 训练入口含 argparse 参数 ├─ data_process/ │ ├─ make_dataset.py # 清洗、切分、缓存 │ └─ vocab.py # 词表构建 ├─ models/ │ ├─ textrank.py # TextRank 抽取与关键词 │ ├─ encoder.py # Seq2Seq encoder │ └─ decoder.py # attention decoder └─ gui/ ├─ main_window.py # PyQt5 主界面 └─ worker.py # QThread 异步任务依赖文件建议写成下面这样python 3.8 环境可以直接一把梭安装# python 3.6-3.8 均可用 torch1.7 numpy jieba PyQt55.15.* tqdm rouge这里有几个安装相关的坑要提前说。PyQt5 在 Windows 上直接用 pip 安装即可在 Linux 下装好后运行经常报缺少 xcb 库需要额外安装 libxcb-* 相关系统包。python 环境变量配置不好时命令行里敲 python 可能启动的是另一个版本导致 pip 安装的 PyQt5 和当前解释器对不上建议全程用 conda 环境隔离。4.2 TextRank 核心代码构建邻接矩阵、迭代排序TextRank 的关键是先把文档按句切分再对每个句子里的候选词做窗口共现统计。候选词要经过词性过滤只保留名词和动词然后才能建图。import jieba import jieba.posseg as pseg from collections import defaultdict STOP_WORDS set([公司, 记者, 报道, 进行, 一个, 我们, 他们]) def build_graph(sentences, window5): # 候选词只保留名词/动词/动名词过滤停用词和单字词 words_per_sent [] for sent in sentences: words [w.word for w in pseg.cut(sent) if w.flag[0] in (n, v) and w.word not in STOP_WORDS and len(w.word) 1] words_per_sent.append(words) # 词到 id 的映射等价于给图的每个节点编号 vocab {} for words in words_per_sent: for w in words: if w not in vocab: vocab[w] len(vocab) n len(vocab) # 邻接矩阵初始化成全零矩阵后面只给共现位置赋值 graph [[0] * n for _ in range(n)] # 窗口内共现每个词与其后 window-1 个词两两计数 for words in words_per_sent: for i in range(len(words)): for j in range(i 1, min(i window, len(words))): u, v vocab[words[i]], vocab[words[j]] graph[u][v] 1 graph[v][u] 1 # 无向图边是对称的 return graph, vocab def textrank_score(graph, d0.85, max_iter200, min_diff1e-4): n len(graph) # 按列归一化分母每个邻居的权重要除以它的出边权总和 col_sum [sum(graph[r][c] for r in range(n)) for c in range(n)] # 节点初始得分平均分配 pr [1.0 / n] * n for _ in range(max_iter): new_pr [(1.0 - d) / n] * n for i in range(n): for j in range(n): if graph[i][j] and col_sum[j] 0: # 邻居 j 的得分按边权比例传给节点 i new_pr[i] d * pr[j] * graph[i][j] / col_sum[j] diff sum(abs(new_pr[i] - pr[i]) for i in range(n)) pr new_pr if diff min_diff: break return pr上面两段代码的逻辑顺序是先由句子里的词构建图和词表再做迭代打分。第 19 行的词性判断是关键如果不过滤停用词和通用名词“公司”“记者”这种高频词会霸榜。初始化邻接矩阵时如果漏掉对称赋值图就变成了有向图矩阵按列归一化时会出现大量零分母运行时会报除零错误。4.3 Seq2Seq 训练generic attention 的 decoder 与 teacher forcingDecoder 是整个 Seq2Seq 的难点注意力模块我用 generic attention 的实现参数少且稳定。以下是可直接运行的 decoder 核心类import torch import torch.nn as nn import torch.nn.functional as F class AttentionDecoder(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim hidden_dim, hidden_dim, batch_firstTrue) # generic attention 参数两个线性层 一个打分向量 self.W_h nn.Linear(hidden_dim, hidden_dim) self.W_s nn.Linear(hidden_dim, hidden_dim) self.v nn.Linear(hidden_dim, 1) def forward(self, inputs, enc_states, hiddenNone): # inputs: (batch, seq_len), enc_states: (batch, src_len, hidden_dim) emb self.embedding(inputs) # (batch, seq_len, embed_dim) # 初版不考虑 context 输入先用全零 context 占位 batch_size, seq_len inputs.size(0), inputs.size(1) ctx torch.zeros(batch_size, seq_len, self.lstm.input_size - self.embedding.embedding_dim, deviceinputs.device) out, hidden self.lstm(torch.cat([emb, ctx], dim-1), hidden) # out: (batch, seq_len, hidden_dim)每一个 decoder 时间步都有一个隐状态 # 计算 attention score # score v * tanh(W_h * h_enc W_s * h_dec) dec_part self.W_s(out) # (b, t, h) enc_part self.W_h(enc_states) # (b, s, h) # 利用广播t 个 decoder 时间步 与 s 个 encoder 位置做加法 score self.v( torch.tanh(dec_part.unsqueeze(2) enc_part.unsqueeze(1)) ).squeeze(-1) # (b, t, s) attn F.softmax(score, dim-1) # 对每个 decoder 步归一化 ctx torch.bmm(attn, enc_states) # (b, t, h) 加权取出原文信息 return ctx, attn, hidden这段代码值得重点看的是第 28 到 32 行的广播加法。dec_part 的形状是 (batch, t, hidden_dim)enc_part 的形状是 (batch, s, hidden_dim)两个三维张量不能直接相加所以要先各加一个维度变成 (batch, t, 1, h) 和 (batch, 1, s, h)广播后才能得到 (batch, t, s) 的注意力分数矩阵。很多人的实现在这里因为少一个 unsqueeze 直接报维度错误这也是 seq2seq pytorch 教程里最常见的翻车点。训练循环里要有一个 teacher_forcing 的选择逻辑每个 batch 以概率 tf_ratio 决定输入 decoder 的是真实目标序列还是上一时刻的预测 token。常见训练参数batch_size64learning_rate0.001epoch20梯度裁剪 5.0。loss 用 CrossEntropyLoss计算时把 padding 位置 weight 设为 0这样模型不会被无意义的占位符带偏。4.4 PyQt5 界面不要在事件循环里跑模型用 QThread 串并联界面布局常见做法是左侧放原文编辑区右侧显示生成的摘要和关键词列表底部放一个“生成”按钮和进度条。这块最容易犯的错是把模型推断直接写进按钮的点击响应函数模型跑起来后 GUI 线程被占死整个窗口变“未响应”。解决办法是把耗时任务放进 QThread主线程只负责接收结果信号。from PyQt5.QtCore import QThread, pyqtSignal class SummaryWorker(QThread): # 信号摘要文本 关键词列表 finished pyqtSignal(str, list) def __init__(self, text, textrank, seq2seq): super().__init__() self.text text self.textrank textrank self.seq2seq seq2seq def run(self): # 子线程中执行抽取和生成入口函数不要碰 GUI keywords self.textrank.extract(self.text) summary self.seq2seq.generate(self.text) self.finished.emit(summary, keywords)在按钮回调里创建 worker 并 start然后把 finished 信号连接到界面更新函数即可。需要连续执行多个任务时可以用 python 队列 queue 把结果排队主线程按顺序取而不要在一个子线程里再开子线程线程嵌套线程的调试成本远高于收益。想用协程和 async 也可以但对这个场景没有额外收益QThread 反而是最省心智负担的方案。界面里给关键词做高亮显示时可以直接用 QTextEdit 的 toHtml 能力把关键词拼成带颜色标签的 HTML 文本喂进去。这个做法比单独写一个 QListWidget 再逐项设置样式更省代码视觉效果也足够清楚。提示启动生成任务后建议把按钮 setEnabled(False)防止用户重复点击导致多个 worker 同时跑结束信号回来再恢复。这一步不写训练和推理资源被挤占后界面会明显变卡。5. 踩坑与排查训练不收敛、GUI 卡死、关键词全是“公司”的 5 条血泪经验复现这类工程时“模型不行”往往不是真正的原因问题大多出在数据构造和调用方式上。以下 5 条按“现象 → 原因 → 解决”记录你按顺序排查能省一半时间。5.1 训练 loss 稳在 2.0 左右不动检查 target 有没有真正带上 [EOS]现象train loss 前几个 epoch 降到 2.0 附近就再也不动生成结果全是短小空洞的短语。原因构造 target 序列时漏掉了 [EOS] 结束符decoder 训练时永远看不到序列该何时终止或者 padding 位被计入了 loss模型发现预测 pad 符号也能降损失。这类问题不动代码看不出来因为 loss 曲线看起来很平滑属于典型的“玄学不收敛”。解决打印一条训练样本的 target确认末尾有 [EOS]并打印 loss 权重矩阵确认 padding 位置权重为 0。修复后 loss 通常会在一到两个 epoch 内继续明显下降。5.2 生成结果反复出现同一句话attention 没学到beam search 权重要调现象无论输入什么文章生成的摘要都输出类似“今天今天今天”或一个固定的短句。原因attention 模块没有真正学会从 encoder 隐状态里取信息context 向量被模型忽略decoder 变成纯语言模型在自说自话。常见诱因是学习率过高、dropout 太小导致训练后期过拟合或者是 teacher forcing 比率设太低导致训练过程不稳定。解决把学习率降到 3e-4dropout 设为 0.2 以上teacher forcing 比率提到 0.8。推理时 beam_size 设 3 到 5并加重复惩罚。改完这几项还不行就去画 attention 热力图看权重是不是真的集中在关键句上。5.3 TextRank 抽出的关键词全是“公司”“记者”词性白名单和停用词不够狠现象关键词列表里全是“公司、报告、记者、进行、一个”真正的主题词一个没出来。原因jieba 词性标注把一些通用名词标成了 n而新闻语料里这类词本身出现频次极高。只靠通用停用词表过滤不干净因为“公司”在新闻里确实是名词。解决除了通用停用词表还要从你自己的语料里统计高频但无意义的词加进自定义停用词表。词性白名单只留 n、an、vn、v过滤单字词并且在窗口建图之前对过于高频的词直接剪掉可以按词频排名把前 20 名强制排除。这类问题没有一劳永逸的参数需要按语料迭代两三次。5.4 点生成按钮后窗口转圈卡死模型放主线程了没走 QThread 队列现象点击“生成”按钮后界面立刻失去响应系统提示“未响应”几秒甚至几十秒后才恢复。原因模型推理代码直接写在按钮槽函数里PyQt5 的事件循环被同步阻塞。GUI 线程根本没有机会重绘界面和处理鼠标事件。解决把耗时的抽取和生成逻辑全部放进 4.4 里的 SummaryWorker用信号把结果传回主线程。如果模型推理耗时不稳定还可以把任务提交到一个队列由一个 worker 线程逐个消费防止任务堆积时界面按钮状态和实际执行状态不一致。5.5 换机器后训练报维度或环境错误python 环境变量配置和依赖版本不一致现象同一份代码在原来的机器能跑换一台机器后不是 torch 报 CUDA 错误就是 numpy 导入失败。原因python 安装版本不一致导致 pip 装到了另一个环境或者 PyTorch 版本与 CUDA 版本不匹配。Linux 机器上尤其常见系统自带的 python 和 conda 的 python 同时存在于 PATH 里python 环境变量配置一乱install 的包和 import 的包根本不是同一个环境。解决统一用 python 3.8 conda 创建独立环境requirements.txt 锁版本GPU 机器单独安装对应 CUDA 版本的 torch。换机器时把整个 conda 环境导出重装不要只拷代码目录。训练缓存数据也一并拷走免得新机器重新分词后词表和原词表顺序不一致导致加载权重时维度对不上。6. 用 ROUGE 打分与 attention 可视化把辅助生成系统变成能交差的工具模型训完只是第一步验证它“到底行不行”最直接的办法是算 ROUGE 分再把 attention 可视化。ROUGE-1 衡量单字重合率ROUGE-2 衡量相邻词组重合率ROUGE-L 看最长公共子序列。对抽取式摘要和生成式摘要分别打分你会发现 TextRank 抽取前三句通常 ROUGE-1 能到 0.3 左右Seq2Seq 训练充分时能到 0.35 上下两者取长补短之后才是一个完整的产品形态。把 attention 权重矩阵画成热力图也很有用。做法是在推理时从 decoder 里把 attn 矩阵存下来横轴是 encoder 输入词纵轴是 decoder 输出词格子颜色越深代表当前输出词越关注对应的输入词。画图时注意 python 画图横坐标太密集的问题如果输入序列超过 100 个词xticks 每隔 5 到 10 个词显示一个标签否则横坐标会挤成一团黑线。一张正常的 attention 热力图应该能看出每一行生成词都有少数几个集中关注的原文词如果全部一行灰色说明 attention 处于崩溃状态。我习惯在界面里做一个双栏对比视图左栏显示 TextRank 选出的关键句并用颜色标出关键词右栏显示 Seq2Seq 生成的摘要下方再给一个 beam search 宽度和摘要长度调节框。这种形态比单纯的文本框更接近“辅助生成工具”的定位演示时说服力也强得多。每次跑完验证集我会把所有 ROUGE-1 低于 0.3 的样本存下来翻一遍翻 badcase 比调参更能暴露问题——有时候模型生成得不好是因为参考摘要本身写得不合理这种情况再训练也不会有进展。这套系统做下来我最深的体会是界面和技术选型都不是瓶颈数据质量才是。TextRank 和 Seq2Seq 都是被验证过无数次的成熟方法把注意力放在数据清洗、弱标签生成和推理参数上比追求新模型划算得多。希望你跑通之后也能有自己的坏案例库那才是真正值钱的东西。希望帮到你。本文还有配套的精品资源点击获取