☰
文本自动摘要实战:从抽取式到生成式,避开五大常见坑
2026/9/30 4:32:32 网站建设 项目流程

简介:一份基于深度学习的文本自动摘要研究方案,内容源自《计算机应用》2019年第2期正式论文,作者单位为北京电子科技学院与西安电子科技大学。方案针对NLP生成式自动摘要中语义理解不充分、摘要语句不通顺、准确度不高等问题,提出了改进的词向量生成技术,在Skip-Gram基础上引入词性、词频和逆文本频率三个特征,并构建Bi-MulRnn生成式摘要模型,融合seq2seq、自编码器、注意力机制、GRU、双向与多层循环神经网络及集束搜索。在大规模中文短文本摘要LCSTS数据集上的实验显示,该方案在Rouge评价体系中表现良好,能有效提升摘要准确性与语句流畅度。资源为PDF格式,共1个文件,压缩包大小约1.06MB,属于参考文献与专业指导类资料,适合从事自然语言处理、文本摘要研究的学生、算法工程师及相关方向科研人员阅读参考,用于了解生成式摘要建模思路、关键技术选型及实验评测方法。已有282人学习下载。

1. 文本自动摘要方案:先算清“压缩”和“重写”这笔账

做基于深度学习的文本自动摘要方案时,最容易翻车的地方不是模型选型,而是没把“压缩”和“重写”这两条路分开。身边不止一个人拿通用大模型跑新闻摘要,效果惊艳,但换到合同、病历、设备日志后输出不可控,最后灰溜溜退回规则。文本自动摘要本质上就两条路:抽取式从原文里挑句子拼成摘要,生成式用模型重新组织内容。深度学习在两条路上都能做,但数据要求、评估方式和踩坑点完全不同。这篇笔记适合正在搭摘要方案、需要给团队一个可复现基线、或者被“ROUGE分数高但没人敢用”折磨的工程师。先定边界,再谈模型。

2. 抽取式还是生成式:方案选型决定后面所有工作量

2.1 摘要任务的两类边界:什么情况选抽取式

抽取式摘要把“摘要”定义成原文句子的子集。优点是忠实度天然有保证,因为每个词都来自原文,不会出现模型编造事实;缺点是句子数量有限,无法像人一样跨句合并和抽象。实际工程里,抽取式适合处理规范性文本,比如会议纪要、合同条款、法律文书,这些场景里“摘出关键句”比“重新组织语言”更安全。

深度学习在这里的角色,常见做法有两种:一是给句子打分,用BERT/CNN这类模型判断每个句子是否该进摘要;二是做序列标注,预测每个句子是保留还是删除。前者更通用,后者在新闻场景表现好。我一般建议先跑无监督的TextRank基线,因为不需要标注数据,能快速拿到一个可行结果,同时用这个基线的ROUGE分数作为后续深度模型的“下限”——如果深度学习模型连这个下限都比不过,就不要上线。

深度学习的另一个价值是能融合上下文。单个句子打分容易选出一堆重复信息,而基于“句子-文档”的交互编码可以抑制冗余。这个后面会提到。另外提醒一句:先搭好深度学习环境配置,跑通一个最简单的句子分类模型,再去调大模型,否则会陷入“环境没配好、代码报错、怀疑业务”的恶性循环。

2.2 无监督基线TextRank:不依赖标注也能跑通的最小方案

TextRank是一种图排序算法,把每个句子当节点,句子之间的相似度当边,迭代计算权重,最后取Top-N个句子。它不需要训练,只需要分词和相似度计算。这决定了它是所有摘要方案里最适合做“先跑通Pipeline”的选择。

import jieba import numpy as np def sentence_similarity(s1, s2): set1 = set(jieba.lcut(s1)) set2 = set(jieba.lcut(s2)) if not set1 or not set2: return 0.0 return len(set1 & set2) / len(set1 | set2) def textrank(sentences, damping=0.85, max_iter=100, tol=1e-6): n = len(sentences) sim = np.zeros((n, n)) for i in range(n): for j in range(n): if i != j: sim[i][j] = sentence_similarity(sentences[i], sentences[j]) row_sum = sim.sum(axis=1, keepdims=True) row_sum[row_sum == 0] = 1e-8 M = sim / row_sum score = np.ones(n) / n for _ in range(max_iter): new_score = (1 - damping) / n + damping * M.T.dot(score) if np.abs(new_score - score).sum() < tol: break score = new_score return score

这段代码有两个关键点。第一,相似度用了分词后的词集合Jaccard,对中文文本基本够用;如果用词向量,效果会好一点,但开销大。第二,转移矩阵M按行归一化,每个句子把自身权重按相似度分给其他句子;damping是PageRank的阻尼系数,通常取0.85,表示从当前节点跳转到任意节点的概率。注意这个相似度矩阵是O(n²)的,文档超过200句就会明显变慢,所以一般先做句子去重,或者用窗口滑动限制连接。

拿到score后,按句子顺序取前K个句子,并按原文顺序输出即可。这个基线不需要任何深度学习环境,但也正是因为它没有学习能力,遇到“摘要在文档中分布比较均匀”的文本时会拉胯——这时才轮到深度学习模型上场。

2.3 把抽取式升级为深度学习模型:训练数据从哪来

深度学习抽取式模型最简单的形态是:对每个句子做二分类,预测它是否属于摘要。但需要一个关键问题——没有人工标注,正负样本怎么构造?常见做法是用ROUGE的Oracle。也就是把原文句子组合起来,和参考摘要做ROUGE比较,选分数最高的组合作为正样本。这里给一个概念验证代码:

from itertools import combinations from rouge_score import rouge_scorer def get_oracle_sentences(doc_sentences, ref_summary, top_k=3): scorer = rouge_scorer.RougeScorer(['rouge1'], use_stemmer=True) best_score = -1 best_combo = [] for k in range(1, top_k + 1): for combo in combinations(doc_sentences, k): cand = ' '.join(combo) score = scorer.score(cand, ref_summary)['rouge1'].fmeasure if score > best_score: best_score = score best_combo = combo return best_combo

注意这段代码只适合句子数少的文档,因为组合数会爆炸。实用做法是贪心:先选一个句子使ROUGE最高,再从剩下的句子里选一个使组合分数最高,迭代直到分数不再提升。用这种Oracle生成句子级别的标签,喂给BERT做句子分类,就能训练一个有监督抽取模型。

这里要强调:抽取式深度模型的上限受限于Oracle标签,如果Oracle本身选的句子就不够好,模型再强也白搭。所以做摘要方案时,先看看Oracle的ROUGE分数,如果它和人工摘要差距过大,说明这个任务用抽取式根本做不好,应该转向生成式。这一步是我在多个项目里检验过的判断标准,能省掉很多无用功。深度学习算法再复杂,也得先回答“任务本身用哪类模型能赢”这个问题。

3. 数据准备与评估:跑通摘要模型前先解决的两件小事

3.1 中文摘要数据清洗:以LCSTS为例

公开数据集中,LCSTS是中文短文本摘要里最常用的一份,字段很简单:短文本和人工摘要。但原始数据不能直接用,里面掺杂着话题标签、@用户和URL,要清洗。常见做法是正则处理:

import re def clean_text(text): text = re.sub(r'#.*?#', '', text) # 去掉#话题# text = re.sub(r'@\w+[::]?', '', text) # 去掉@用户 text = re.sub(r'http\S+', '', text) # 去掉URL text = re.sub(r'\s+', '', text) return text.strip()

清洗后还要做几件事。第一,过滤长度:正文短于30字或长于500字的样本会导致训练时padding浪费,或者没有足够信息生成摘要。第二,过滤摘要和正文重合度过低的样本——生成式摘要允许抽象,但完全没重合的样本在初期会干扰模型。第三,统一用标点切句,而不是用模型切句,避免把工程复杂度带到数据清洗阶段。

这一步的重要性经常被低估。我见过有团队直接用原始微博数据训练,结果模型学会了输出话题标签和表情符号,原因是数据噪声被当成了特征。深度学习模型对数据分布极其敏感,清洗质量比模型结构更能影响最终效果。所以我的习惯是:先把清洗脚本写到数据管线里,每次训练前都跑一遍,绝不用手动清洗的临时文件。

3.2 ROUGE评估:为什么它是摘要任务的唯一通用尺子

摘要任务不像分类任务有准确率,因为“好摘要”没有唯一答案。业界通用做法是用ROUGE,它统计生成摘要和参考摘要之间n-gram的重合度。ROUGE-1衡量单字/词重合,ROUGE-2衡量二元组,ROUGE-L基于最长公共子序列。计算方式很简单:

from rouge_score import rouge_scorer scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True) scores = scorer.score(pred_summary, ref_summary) print(scores['rouge1'].fmeasure)

use_stemmer会把英文单词还原,中文场景一般开着影响不大。ROUGE的问题也很明显:它只看词面重合,不看语义。生成式摘要只要换一种说法,词面重合度就低,但内容可能完全正确;反过来,模型直接复制原文句子,ROUGE分数会很高,但字数多、信息冗余。所以ROUGE只能当筛选器,不能当验收标准。

实际项目中,我一般用ROUGE做两件事:一是比较不同模型时看相对差距,比如基线是30,新模型是33,说明有提升;二是配合长度约束,比如生成摘要超过128字时,ROUGE-L会因截断而虚高。要规避这个问题,应该在评测时统一对输出做截断,或者把输出长度也记录下来,人工抽查时重点关注长摘要。

3.3 数据切分与小样本冒烟测试

深度学习的训练过程很长,如果跑到一半发现数据格式错误或者预处理漏了字段,代价很大。所以我的流程是:先把完整数据集切成训练/验证/测试,比例8:1:1,然后只取一小部分跑通全部流程。

from datasets import Dataset def split_dataset(dataset, train_ratio=0.8, val_ratio=0.1): total = len(dataset) train_end = int(total * train_ratio) val_end = int(total * (train_ratio + val_ratio)) return dataset.select(range(train_end)), dataset.select(range(train_end, val_end)), dataset.select(range(val_end, total)) # 冒烟测试:取200条训练、50条验证,确认数据流没问题再上全量 small_train = train_dataset.select(range(200)) small_val = val_dataset.select(range(50))

切分时要注意:摘要任务里同一个文档可能有多个参考摘要,如果按文档ID去重后再切,能避免数据泄漏。这个问题在新闻摘要里经常出现,同一篇新闻被不同媒体改写参考摘要也不同,但正文其实同源,不按文档ID去重会导致验证集分数虚高。

小样本冒烟测试的目的不是看模型效果,而是确认三件事:数据能送进模型、loss能下降、生成结果不是空字符串。这一步能暴露八成环境问题,比如CUDA版本不匹配、tokenizer词表异常、label维度错误。等冒烟测试通过,再切到全量训练,比直接跑一夜然后发现loss为NaN要舒服得多。

4. 用深度学习模型训练摘要:从Encoder-Decoder到生成参数

4.1 生成式摘要的最小原理:让模型学会“重写”

生成式摘要的核心是序列到序列(Seq2Seq)结构:一个Encoder读入原文,一个Decoder逐词生成摘要。早期用LSTM,但长文本信息衰减严重;后来引入Attention,Decoder在生成每个词时都能“回头看”原文的相关位置。现在主流做法是直接用Transformer,把原文和摘要都切成token子词序列,Encoder与Decoder不再共享参数。

深度学习模型在文本摘要上的优势,是可以学习“压缩”的规则:哪些词该保留、哪些冗余词该删除、如何把分散的信息合并成一句。但这种能力需要大量“原文-摘要”对。所以生成式模型的样本需求远高于抽取式,没有几万条高质量标注数据,效果会很不稳定。我见过在几千条样本上硬训Transformer的团队,最后生成结果几乎是把原文前几句抄一遍,和抽取式没区别。

4.2 最小可训练配置:用HuggingFace Trainer跑一个生成式模型

在深度学习环境配置完成后,最快落地生成式摘要的方式是用Transformers库。它提供统一的Tokenizer和Model接口,省去手写注意力机制的麻烦。下面是一段最小训练代码,不针对特定模型,而是给出通用结构:

from transformers import ( AutoTokenizer, AutoModelForSeq2SeqLM, Seq2SeqTrainingArguments, Seq2SeqTrainer, DataCollatorForSeq2Seq ) tokenizer = AutoTokenizer.from_pretrained("your_chinese_summary_model") model = AutoModelForSeq2SeqLM.from_pretrained("your_chinese_summary_model") def preprocess(examples): model_inputs = tokenizer(examples["text"], max_length=512, truncation=True) with tokenizer.as_target_tokenizer(): labels = tokenizer(examples["summary"], max_length=128, truncation=True) model_inputs["labels"] = labels["input_ids"] return model_inputs training_args = Seq2SeqTrainingArguments( output_dir="./sum_model", learning_rate=3e-5, per_device_train_batch_size=8, per_device_eval_batch_size=8, predict_with_generate=True, generation_max_length=128, generation_num_beams=4, max_steps=2000, evaluation_strategy="steps", eval_steps=200, save_steps=200, )

这段代码有两点要说明。第一,tokenizer和model的路径要替换成实际使用的生成模型,但必须保证两者词表一致,否则输出会出现大量[UNK]。第二,predict_with_generate=True表示验证阶段用beam search生成结果,而不是直接取Decoder输出的最大概率词,这样验证的ROUGE才和推理一致。很多项目的评估分数虚高,就是因为训练时没开这个开关。

4.3 训练与推理的哈姆雷特问题:beam search、长度惩罚与重复惩罚

生成式摘要的训练通常用teacher forcing:Decoder每一步都输入真实摘要的 token,模型只预测下一个 token。但推理时没有真实token,模型必须吃自己生成的内容。这个差异是摘要模型“训练正常、推理崩”的根源。

推理阶段的常用做法是beam search,维护K个候选序列,每一步扩展后保留概率最高的K条。但这会引入一个副作用——模型为了追求整体概率,容易反复生成同一句话。所以我一般会在generate参数里显式抑制重复:

outputs = model.generate( input_ids=input_ids, max_length=128, num_beams=4, no_repeat_ngram_size=3, length_penalty=1.0, early_stopping=True, )

no_repeat_ngram_size=3表示生成过程中不允许出现任意连续3个token的重复,能有效缓解“车轱辘话”。length_penalty=1.0是长度惩罚系数,大于1倾向生成长摘要,小于1倾向于短摘要;如果发现模型总把原文整段抄出来,把它调到0.8左右。这些参数需要在验证集上做网格搜索,而不是凭感觉固定。

5. 避坑:文本自动摘要模型的五个常见翻车点

5.1 生成摘要反复重复一句话

现象:模型输出的摘要里连续出现“中国企业中国企业中国企业”,或者某个短语循环出现,整体看起来像复读机。

原因:beam search在概率上偏好重复结构,而训练数据里某些高频n-gram被过度强化。尤其当文本长度超过训练时的最大长度时,模型更容易进入重复循环。

解决:先在推理阶段加no_repeat_ngram_size=3,观察重复是否消失。如果训练数据本身重复模式严重,就要在数据清洗时把原文中的重复片段去掉,或者在训练时使用unlikelihood损失,降低重复token的概率。我一般先调推理参数,解决不了再动训练目标。

5.2 生成摘要比原文还长

现象:设置max_length=128,结果模型生成了128个token的摘要,但正文才80个token,摘要比原文还长。

原因:训练时的摘要长度分布和推理时的max_length设置不一致。如果训练数据里有很多长摘要,模型会倾向生成长内容,而长度惩罚设置不当会导致截断在句中被砍断。

解决:检查训练集摘要长度的分位数,把max_length设为90%分位数附近;同时把length_penalty适当调低。另外评估时不要把截断后的长摘要直接算ROUGE,要记录原始长度,防止用长度换取分数。

5.3 训练loss一直在降,推理结果却是乱码

现象:训练集和验证集loss都正常下降,但模型推理输出里出现大量[UNK]或重复的特殊符号。

原因:这一步最常见的问题是词表不匹配。训练时用了A模型的tokenizer,保存的模型却是从B模型加载的,或者as_target_tokenizer没有正确设置,导致Decoder端的token id错位。

解决:重新检查tokenizer和model的路径,确认来自同一个预训练目录。再用一句测试文本分别调用tokenizer和model,检查input_ids和解码结果能不能对应上。这种问题在深度学习项目里属于“环境配置黑匣子”,跑一个小样本推理脚本就能定位。

5.4 专有名词和未登录词全部消失

现象:摘要里本该出现的公司名、人名被替换成通用词,或者直接省略。比如“英伟达发布新一代GPU”被生成成“企业发布新产品”。

原因:生成式模型受限于词表和训练数据,低频专名容易在生成时被“记忆”替代。抽取式模型反而不会出这个问题,因为它是从原文里选句子。

解决:如果业务场景中专有名词很多,优先考虑抽取式方案。或者用Pointer-Generator思想,在Decoder计算一个“生成/复制”开关,直接从原文复制token。工程上更简单的做法是:生成完成后做一次规则后处理,把原文中出现的专有名词与摘要做匹配补回。

5.5 ROUGE分数高,但人工阅读完全不通过

现象:验证集ROUGE-2达到25,比基线高不少,但业务方抽样一看,发现摘要要么是信息堆砌,要么把最关键的结论漏掉了。

原因:ROUGE是词面重合,模型可能通过复制大量原文句子来刷分,而不是真正理解信息层级。这本质上是自动评估指标与真实质量脱节。

解决:在项目上线前一定要加人工评估环节,至少抽样200条,从事实一致性、信息完备性、通顺度三个维度打分。如果人工分和ROUGE排名不一致,以人工分为准。这个坑我踩过好几次,最后形成的习惯是:ROUGE只能用来筛选候选模型,不能用来验收。

6. 进阶:用质量三角和坏例迭代把方案推上线

人工评估是摘要方案能否上线的最后一道关卡。我常用的评估表只有三个维度:第一,事实一致性,看摘要里是否有原文没有的信息;第二,信息完备性,看核心结论是否被覆盖;第三,流畅度,看句子是否通顺。每个维度1到5分,抽样时把生成摘要和原文放在一起,让人不用看模型名字直接打分。多做几轮后,你会发现分数低的原因高度集中在一两类坏例上。

拿到坏例后,把它们单独存成一份dev-set,每次模型迭代后先跑这份坏例集,看有没有解决。比如某次迭代解决了“重复”问题,但引入了“事实错误”,那下一次训练就要在数据里加入更多事实不一致的负样本,或者把训练目标从交叉熵换成对比学习的形式。这种“坏例驱动”的迭代方式,比我早期追求评测集平均分要有效得多。

还有一个验证技巧:把模型生成的摘要反推回文档,用“摘要能否作为检索关键词召回原文”来间接判断信息量。如果摘要里的关键实体足够多,召回率会高,说明摘要没有丢掉核心信息。这个指标虽然不够严谨,但实现成本低,适合放在监控面板里每天看。

我现在的习惯是:每一版新模型都做一次人工评估,保留所有坏例到固定目录,同时记录当时的生成参数和训练日志。等到项目复盘时,翻这些记录比翻任何实验总结都直观。深度学习模型的玄学往往就藏在这些细节里,希望这些踩坑经验能帮你少走一段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询