
简介PDF文档《基于深度学习的文本自动摘要方案》聚焦自然语言处理中的生成式自动摘要难题面向NLP研究者、深度学习工程师及需要快速获取文献核心方法的读者。文档系统介绍了一种改进的词向量生成技术以Skip-Gram为基础引入词性、词频和逆文本频率三个词特征有效提升词语理解同时提出基于seq2seq与自编码器结构的Bi-MulRnn生成式摘要模型融入注意力机制、门控循环单元、双向及多层循环神经网络和集束搜索改善摘要准确性与语句流畅度并给出LCSTS数据集上的实验设置与Rouge评价结果。资源为单份PDF文件容量1.06MB已有282人学习浏览。内容完整呈现了方案设计思路、模型结构图及关键实现细节适合作为论文写作参考、课堂讲义或算法复现依据。1. 把摘要当翻译做这篇深度学习方案到底解决了什么文本自动摘要这个方向中文开源方案一直有个尴尬抽取式做出来像“句子拼盘”生成式又卡在语义理解和语句通顺上。这篇《基于深度学习的文本自动摘要方案》是《计算机应用》2019年的一篇实战型论文作者张克君、李伟男等人核心思路很直接——把摘要生成当成“原文到摘要”的序列映射任务用seq2seq加注意力机制来做同时把词性、词频、逆文本频率三个特征拼进词向量并在解码端引入邻近词表和集束搜索。它在LCSTS中文短文本摘要数据集上把Rouge-1做到了29.91比单纯RNN加注意力的基线提高了约3个百分点。对正在做中文摘要毕设、舆情系统预研、或者想给NLP入门找个可复现基线的人来说这篇论文的模型结构和调参记录都值得拆一遍。2. 先立框架seq2seq生成式摘要的原理与词向量改进2.1 为什么生成式摘要要选序列映射框架抽取式摘要的做法是从原文里挑现成句子拼起来实现简单但语句之间容易上下文断裂。生成式摘要需要先理解全文主旨再逐词生成新的摘要句本质上是把一个输入词序列映射成另一个输出词序列也就是序列映射问题。序列到序列框架就是为这类问题设计的机器翻译、语音识别、视频字幕都是它的成熟应用场景。在摘要任务里编码器负责把原文读成语义向量解码器负责逐词生成摘要。Rush等人率先把卷积神经网络用在生成式摘要上验证了可行性Chopra等人换成RNN后效果更好Nallapati等人进一步在词特征、停用词和文档结构上做文章说明单纯堆神经网络不够必须把任务相关的特征加进去。这篇论文走的也是这条路——框架用seq2seq自编码器改进点集中在词向量质量和解码器词汇表上。RNN在序列建模上的天然优势是能记住时序信息这是CNN做不到的。CNN需要额外加位置信息来模拟时序参数又多所以在摘要这种对词语顺序敏感的任务里RNN是更合理的选择。作者在编解码器里选了GRU门控结构而不是LSTM理由也很务实GRU参数更少、更不容易过拟合这在Chung等人的实验里已经有结论。2.2 改进词向量把POS、TF、IDF拼进Skip-Gram输出传统词向量训练只关注词语的共现关系Skip-Gram根据当前词预测上下文词CBOW则反过来。这两种方法都没有利用词性、词频这类语言学特征。摘要句一般是中性陈述很少带情感色彩形容词和副词出现频率低真正承载信息的是名词和动词。所以作者在Skip-Gram生成的词向量后面直接拼接了词性、词频、逆文本频率三个离散化特征。import numpy as np from collections import Counter def build_enhanced_vector(word, base_vector, pos_tag, tf, idf, pos_dim10, stat_dim5): 拼接基础词向量与词性/词频/IDF特征 base_vector: Skip-Gram预训练词向量shape(emb_dim,) pos_tag: 词性标注如n,v,a tf: 该词在当前文本中的出现次数 idf: 该词在语料库中的逆文本频率 # 词性离散化用固定映射编码成one-hot的压缩形式 pos_dict {n: 0, v: 1, a: 2, r: 3, d: 4, UNK: 5} pos_vec np.zeros(pos_dim) pos_idx pos_dict.get(pos_tag, 5) pos_vec[pos_idx % pos_dim] 1.0 # 词频和IDF做归一化后拼接 stat_vec np.array([min(tf / 100.0, 1.0), 1.0 / (1.0 np.log(idf 1e-6))]) stat_padded np.pad(stat_vec, (0, stat_dim - 2)) enhanced np.concatenate([base_vector, pos_vec, stat_padded]) return enhanced这段代码的逻辑是把词性映射成稀疏向量词频做归一化IDF取对数压缩后拼到基础词向量末尾。词性特征帮助模型区分名词和形容词的语法角色TF反映词在原文中的重要程度IDF则抑制“的、了、是”这类语料级高频词。离散化映射的位数不需要太多词性类别本身只有几十种压缩到10维足够表达。TF归一化除以100是为了把常见的高频词拉到和低频繁词一个量级避免数值波动过大。这里有个值得注意的细节三个特征之间量纲差异很大词向量本身是浮点数分布TF是整数计数IDF是对数值。直接拼接前必须做归一化否则模型会过度关注数字大的维度。作者没在论文里展开归一化方式从工程角度我一般会先统计训练集上的TF分布再决定缩放系数。2.3 邻近词表解码器词汇表不能只有原文词解码器词汇表的大小直接决定softmax计算量这是整个模型的速度瓶颈。传统做法是用目标语言全量词表计算成本太高。作者提出了邻近词表技术词表由三部分组成——原文词汇、语料高频词、以及原文词汇在高维词向量空间中的邻近词。邻近词的概念值得展开。自动摘要和机器翻译有个本质区别翻译的输入输出词汇基本一一对应但摘要的标准答案里经常出现原文没有的词。原文说“在排名中最高”标准摘要可能写“居榜首”两者意思相同但用词不同。如果解码器词表里只有原文词模型永远无法生成“榜首”这个词。邻近词表的做法是在词向量空间里找原文高频词的余弦距离最近的几个词把这些词也放进解码器词表。from sklearn.metrics.pairwise import cosine_similarity def build_neighbor_vocab(original_tokens, high_freq_words, embedding_matrix, neighbor_k3, vocab_size4000): 构建邻近词表原文词 高频词 邻近词 embedding_matrix: shape(vocab_size, emb_dim)预训练词向量矩阵 neighbor_set set() for token in original_tokens: if token not in embedding_index: continue token_vec embedding_matrix[embedding_index[token]].reshape(1, -1) sims cosine_similarity(token_vec, embedding_matrix)[0] top_k sims.argsort()[::-1][1:neighbor_k 1] # 排除自身 for idx in top_k: neighbor_set.add(vocab_list[idx]) decoder_vocab list(original_tokens) list(high_freq_words) list(neighbor_set) # 截断到目标大小优先保留原文词和高频词 decoder_vocab decoder_vocab[:vocab_size] return decoder_vocab代码里neighbor_k是邻近度论文里设定为3即取余弦值最接近的3个词。要注意的是邻近词的计算是基于预训练词向量矩阵的不是基于当前任务训练出来的向量。所以预训练语料的质量直接决定邻近词靠不靠谱。用通用中文维基语料训练的向量邻近词基本是语义相近的同义词这个效果是最理想的。3. 模型结构拆解Bi-MulRnn的编码器、解码器与注意力3.1 编码器用双向RNN解决注意力偏后的问题传统单向RNN有个固有问题随着时间步推进早期输入的信息会在反向传播中逐渐衰减网络对句子开头的词不敏感。摘要任务里关键信息往往分散在全文各处如果编码器只强化了句尾信息解码器生成摘要时就容易漏掉开头的重要实体。作者在编码器采用了双向循环神经网络。每个时刻的词同时送入前向和后向两个GRU单元前向单元从句子开头读到结尾后向单元从结尾读到开头每个时刻的隐层状态融合两个方向的信息。第i个时刻的隐层输出由公式hi [fi; gi]拼接而成其中fi GRU_f(xi, f(i-1))gi GRU_g(xi, g(i1))。这样每个词都能同时看到它左右的上下文语义向量的信息密度更高。从实现上说双向RNN并不复杂PyTorch里用nn.GRU(..., bidirectionalTrue)一行就能搞定。但需要注意拼接维度双向GRU的输出shape是(seq_len, batch, num_directions * hidden_size)后续接注意力层时要按最后一维拆开或直接整体使用。编码器的最后一层隐状态需要单独取出前向和后向的末位拼接作为解码器的初始状态。3.2 解码器用三层RNN为什么只有顶层和注意力交互解码器这边作者用了多层循环神经网络具体是三层GRU堆叠。每一层接收前一层在上一时刻的隐状态作为本层当前时刻的额外输入层数加深提升了模型的泛化能力。但有个关键细节只有第三层最顶层和注意力机制交互。下面两层专注于建模摘要句内部的语序和语法结构顶层负责从编码器语义向量中提取当前时刻需要关注的信息。import torch import torch.nn as nn class BiMulRnnDecoder(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_size, num_layers3, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.gru nn.GRU( embed_dim, hidden_size, num_layersnum_layers, dropoutdropout if num_layers 1 else 0.0 ) self.attn_proj nn.Linear(hidden_size * 2, hidden_size) # 编码器输出维度可能翻倍 self.vocab_proj nn.Linear(hidden_size, vocab_size) def forward(self, encoder_outputs, decoder_input, hiddenNone): embedded self.embedding(decoder_input) output, hidden self.gru(embedded, hidden) # 只有顶层输出参与注意力计算output[:, -1]即第三层输出 top_output output[:, -1, :] attn_weights self.attn_proj(encoder_outputs.transpose(0, 1)) attn_weights torch.softmax(attn_weights top_output.unsqueeze(-1), dim1) context torch.sum(attn_weights * encoder_outputs.transpose(0, 1), dim1) logits self.vocab_proj(top_output context.squeeze(-1)) return logits, hidden这段代码的attention_score用的是论文公式里的score(st, hk)函数把编码器输出经过线性投影后与解码器顶层隐状态做点积softmax归一化后得到每个编码时刻的注意力权重。context向量是加权求和的结果拼回解码器输出后再映射到词表维度。多层解码器的trick在于hidden状态的传递第一层接收上一时刻的output输出作为第二层输入依此类推。PyTorch的nn.GRU封装了这种层间传递只需要传入num_layers3。但如果你要自己实现每层的递推就要注意层间隐状态的维度对齐第一层的初始状态由编码器的双向末位拼接而来shape是(num_layers * num_directions, batch, hidden_size)。3.3 注意力机制的score函数怎么选注意力机制是这套方案的另一个关键。摘要生成时模型预测每个词需要回顾原文的不同部分。如果没有注意力机制解码器每次只能依赖编码器的最终状态高频词会掩盖真正重要的信息连续预测出同一个词的情况并不少见。作者采用全局注意力机制的通用方法score函数选择的是加性注意力还是点积注意力论文公式里score(st, hk)没有明确写死。在做实验时我一般先试点积注意力因为矩阵乘法效率高如果效果不理想再换成加性注意力。全局注意力和局部注意力的差别在于全局注意力每次预测时都要扫描编码器的全部时间步局部注意力只关注一个窗口。作者说针对短文本两者区别不大局部注意力反而更复杂所以选了全局。这里有个工程上的体会LCSTS的文本平均长度只有几十个字全局注意力的计算开销完全可以接受选全局是合理的。但如果迁移到长文档摘要全局注意力每次扫描全文的成本会线性增长届时需要考虑窗口机制或者分层编码。3.4 集束搜索和训练参数生成时不能贪心摘要生成的最后一步作者用了集束搜索束大小设定为7。集束搜索的原理是每步保留概率最高的K个候选序列而不是只留一个。贪心解码每步只取概率最大的词一旦某步选错就回不了头。束搜索相当于保留了多条候选路径最后从K个候选中选整体概率最高的那个。束越大搜索空间越大结果理论上越好但计算代价也越高。作者实验里束大小设7常见的介于5到10之间可以根据显存和时延调。训练参数方面论文给出了完整配置词向量维度250批尺寸50初始学习率1.0学习率更新用Adadelta词汇表从jieba分词后的文本中取60000个高频词超出部分用UNK表示解码器词表大小限制在4000。训练在Tesla P4上跑了将近一周。这批参数里最值得关注的是Adadelta它不需要手动调整学习率衰减策略会自动调节每个参数的学习率比较省心。批尺寸50在当年算适中的显存紧张的可以降到32但要注意梯度的稳定性会受影响。4. LCSTS数据集上的实验与Rouge指标解读4.1 数据集结构训练、验证、测试怎么划分实验用的大规模中文短文本摘要数据集LCSTS有三个部分。Part1包含2400591条文本-摘要对直接作为训练集。Part2是从Part1中随机采样10666条分5份由5位志愿者按“文本与摘要相关程度”从1到5打分5表示最匹配。Part3是三位志愿者同时对2000条数据标注从中挑选出三个标注一致的数据得到1106条这部分不包含在Part1和Part2里可作为测试集使用。实际操作时要注意Part2的相关程度分数也可以用来做验证集筛选。论文测试时选了Part3中评分在3分以上的原文-摘要组合这个筛选逻辑在复现时别漏掉——如果直接用全部1106条做测试低分样本会拉低Rouge指标。表1给出了详细的数据分布Part1总数2400591条Part2总数10666条Part3总数1106条。4.2 训练细节与超参数复现需要盯住哪些值复现这篇实验有几个参数直接影响收敛速度和质量。分词工具用jieba这决定了编码器词汇表的分词粒度。60000个高频词组成编码器词表词表外的词用UNK标记。解码器词表大小4000先加入原文词汇再取邻近词汇最后用编码器词表中的高频词填充剩余位置。邻近度设定为3即每个词取余弦值最接近的3个词。超参数设定值说明词向量维度250Skip-Gram预训练含拼接特征后实际输入维度更高批尺寸50每批50个样本显存不足可降初始学习率1.0配合Adadelta自动调节学习率更新Adadelta无需手动衰减编码器词表60000jieba分词后取高频词解码器词表4000原文词邻近词高频词邻近度3词向量空间余弦距离最近的3个词束大小7集束搜索宽度词向量维度250这个数值在当年是偏小一点的。现在做中文摘要任务用300维或更高维度的词向量更常见。但维度不一定是越大越好——维度越高需要更多训练数据来拟合词向量分布小数据集上反而容易过拟合。LCSTS有240万对训练数据250维是够用的。4.3 Rouge评测中文摘要怎么用Rouge工具包Rouge评价体系是目前自动摘要的公认标准分析候选摘要和专家摘要在字、词、句子层面的重合度。论文用了Rouge-1、Rouge-2和Rouge-L三种方式Rouge-1基于字的相似度Rouge-2基于词的相似度Rouge-L基于最长公共子序列衡量语句流畅度。这里有个中文特有的坑标准Rouge工具包是为英文设计的按空格分词。中文没有天然空格直接拿标准工具包跑分出来的词会错得离谱。作者的解法是把中文字符编码成英文字符串这样每个中文字符对应用一个英文字母字符变相完成了“以字为单位”的评测。这个技巧特别实用复现时直接用字符级别的Rouge评测函数即可不用真去转编码。from rouge_score import rouge_scorer def evaluate_cn_summary(reference, candidate): 中文摘要的Rouge评测 将中文字符转为英文字符按字符单元计算 # 中文字符转码每个汉字映射为一个唯一字母组合 def zh_to_en(text): mapping {} idx 0 result [] for ch in text.strip(): if ch not in mapping: mapping[ch] chr(97 (idx % 26)) str(idx // 26) idx 1 result.append(mapping[ch]) return .join(result) scorer rouge_scorer.RougeScorer([rouge1, rouge2, rougeL], use_stemmerFalse) scores scorer.score(zh_to_en(reference), zh_to_en(candidate)) return scores转码后每个汉字会被映射成完全不同的字符串Rouge工具包按空格分隔字符时理论上不会出现多个汉字映射到同一个字符而干扰计算的情况。最终实验数据对比是这样的RNN基线Rouge-1为17.70、Rouge-2为8.50、Rouge-L为15.80加入注意力机制的RNN context提升到26.80、16.10、24.10Bi-MulRnn双向编码器加多层解码器加注意力达到27.86、17.17、25.95完整版Bi-MulRnn再加改进词向量和邻近词表是29.91、19.68、28.08。模型Rouge-1Rouge-2Rouge-LRNN17.708.5015.80RNN context26.8016.1024.10Bi-MulRnn27.8617.1725.95Bi-MulRnn29.9119.6828.08从数字看注意力机制的引入是一次大的量级跳跃提升了大约9个百分点双向编码器、多层解码器、词向量增强和邻近词表各贡献2到3个百分点的增量。Rouge-2的提升尤其值得注意它衡量的是词对级别的匹配说明生成式模型的词汇选择准确性确实在变好。5. 避坑指南复现这个模型最容易翻车的五个地方5.1 现象生成的摘要全是UNK根本没法看原因编码器词汇表只取了60000高频词原文里的专有名词、生僻词大量落到了词表外被统一替换成UNK标记。解码器输出时如果模型无法在词表中找到合适词会倾向于生成UNK。解决除了扩充词汇表更有效的办法是调整词表截断策略。60000这个数字是基于语料统计的但不同领域文本的词频分布差异很大。我的做法是先对训练集做分词统计观察累计覆盖率——不一定要覆盖到99%以上才算够用但至少要保证测试原文里的核心名词不被UNK覆盖。另外在解码器端遇到UNK时可以做一个指针回退直接把编码器对应位置的原文词复制到摘要输出里很多摘要模型都采用这种pointer-generator机制。5.2 现象邻近词表生成一堆语义完全无关的词原因邻近度设成3之后在词向量空间里找余弦距离最近的3个词惩罚假设了高质量词向量。如果预训练词向量语料和LCSTS文本领域差异大比如用新闻语料训练的向量来处理医学文本余弦距离最近的词根本不是近义词。解决换用领域匹配的预训练词向量或者干脆用LCSTS自身的文本从头训练词向量。另一个办法是校验邻近词的质量——筛选邻近词时加一个词性一致性约束比如原文词是名词邻近词也必须是名词。这样能过滤掉“高→低”这类形容词对名词的干扰。5.3 现象训练loss下降正常但Rouge指标死活上不去原因这是摘要任务最常见的隐形坑——训练用的是最大似然估计每一步都在最大化当前词的概率而评测时用的是Rouge这种基于重合度的指标。两者目标不一致训练loss很低不代表生成出的句子在字面层面和参考摘要高度重合。解决训练到后期加入基于Rouge的强化学习微调或者直接用对比学习拉近生成摘要和参考摘要的语义向量距离。如果只想简单提升先试增大束搜索宽度从7调到10Rouge通常会有小幅度提高。另外检查解码端是否做了长度归一化不平滑长度惩罚时模型容易生成过短的摘要。5.4 现象训练过程直接OOMTesla P4都顶不住原因双向编码器加三层解码器的参数量并不大但attention层要维护某个时刻对所有编码器隐状态的打分序列长度一旦超过一两百字中间矩阵的显存占用是平方级增长。解决短文本任务可以把max_seq_len限制在80或100LCSTS的原文长度大多数在50字以内。如果要做长文本需要引入truncated attention或者按句切分后做层次编码。另一种做法是采用混合精度训练半精度浮点能省近一半显存代价是需要小心数值稳定性建议loss缩放用动态策略。5.5 现象Rouge分数上下浮动超过1个百分点没法稳定复现原因训练集的随机采样顺序被不少人忽略。模型训练时训练样例的选取是随机的这会导致不同次实验的收敛点不同。再加上Adadelta的初始epsilon参数敏感小小的epsilon差异会在训练后期放大。解决复现时固定随机种子包括Python的random、NumPy的random和PyTorch的manual_seed。评估时多次运行取平均值或者至少在报告里注明单次运行的波动范围。工程上部署时把训练好的模型参数固化生成阶段不涉及随机性结果可以做到完全可复现。6. 进阶把Bi-MulRnn从短文本迁到长文档摘要6.1 编码器升级分层注意力替代单层双向RNN短文本的编码器直接对整个序列读一次就够了但长文档动辄上千字单层双向RNN无法建模段落间的层次结构。我会把编码器拆成两层第一层用word-level的双向RNN编码每个句子内部的词序列得到句向量第二层用sentence-level的双向RNN编码句子序列得到文档向量。注意力机制在解码时先选句子再选句子里的词两步注意力能显著降低长文档的定位难度。这个改动对模型参数量的增加有限但对长文本摘要的改善非常明显。6.2 解码端增加重复惩罚与n-gram阻断短文本摘要里“连续重复预测同一个词”的问题在长文档生成中会更严重因为文档越长解码器越容易陷入循环。我在解码阶段会让集束搜索配合一个惩罚项当某个词在当前已生成的序列中出现过时将其softmax概率乘以一个小于1的系数比如0.8。再加上n-gram阻断——禁止生成包含重复2-gram序列的候选句——能有效压制循环重复。def generate_with_repetition_penalty(model, encoder_context, max_len100, penalty0.8): 带重复惩罚的集束搜索解码 penalty 1 时已出现词的概率被压低 generated [] beam [([], 0.0)] # (token序列, log_prob) for _ in range(max_len): new_beam [] for tokens, score in beam: if len(tokens) max_len: continue logits model.decode_step(encoder_context, tokens) probs torch.softmax(logits, dim-1) # 重复惩罚已经出现过的token概率打折 for t_idx in set(tokens): probs[t_idx] probs[t_idx] * penalty top_k torch.topk(probs, k7) for idx, p in zip(top_k.indices, top_k.values): new_beam.append((tokens [idx.item()], score torch.log(p))) new_beam.sort(keylambda x: x[1] / len(x[0]), reverseTrue) beam new_beam[:7] generated beam[0][0] if beam[0][0][-1] 2: # EOS token id 假定为2 break return generated迁移到长文档后Rouge整体会下降一些——这实属正常因为长参考摘要的词汇空间更大。实践中把邻近词表从4000扩到8000、邻近度从3调到5通常能挽回一到两个点。另一条经验是词性特征在长文档里的作用更突出因为长文档的谓词结构更复杂模型对动词和名词的区分能力影响更大。从这篇论文往后我做自动摘要实验都会强制走一遍固定流程先把随机种子定死、确认中文分词一致性、再检查解码器词表里有没有足够的邻近词兜底。这三步走完模型再差也在可控的范围内剩下的就交给调参了。希望这篇拆解对你复现或改造自己的摘要模型有实际帮助。本文还有配套的精品资源点击获取