摘要冗余是文本自动化处理中非常典型的质量问题。一段摘要如果只是简单地把原文几段话拼在一起,不仅信息密度低,还会出现大量重复表述,读者抓不住重点。要解决这个问题,需要从两个层面入手:一是关键句提取,判断哪些句子值得进入摘要;二是文本压缩,对选出的句子做进一步精简,去掉冗余成分,只保留信息量最高的部分。本文将结合原理和代码,完整讲解这两条技术路线。

为什么摘要会产生冗余
冗余的来源主要有三类。第一类是句间冗余:原文中多个句子表达的是同一件事,比如新闻稿里第一段和最后一段都在重复核心事件,如果抽取算法不加以判断,两句都会被选进摘要。第二类是句内冗余:单个句子里包含大量修饰语、背景说明和过渡词,比如“值得一提的是,据相关媒体报道,该事件在当时引起了广泛关注”这句话里,真正有信息量的只有“该事件引起了广泛关注”。第三类是结构冗余:摘要缺少层次组织,把细节和结论混在一起,导致篇幅膨胀而信息量没有增加。
理解冗余来源后,对应的解决思路就清晰了:句间冗余靠关键句提取阶段的最大边际相关(MMR)策略来控制,句内冗余靠压缩式摘要来处理,结构冗余则需要先对句子按重要性排序再组织输出。下面分别展开。
关键句提取的三种经典方法
关键句提取是抽取式摘要的核心,常见方法包括基于词频的打分、TF-IDF加权和TextRank图排序算法。基于词频的方法最简单:统计文档中每个词的出现次数,一个句子的得分等于它包含的所有词的词频之和,再除以句子长度做归一化。这种方法实现成本低,但对停用词敏感,需要先过滤掉“的”“了”“是”这类高频虚词。
TF-IDF在词频的基础上引入了区分度概念:一个词如果在当前文档频繁出现,但在其他文档中少见,它就更可能是关键词。用它给句子打分,能让包含特色词汇的句子获得更高权重。TextRank则是图排序思路,把每个句子作为图中的一个节点,两个句子之间的相似度(比如词重叠度)作为边的权重,然后迭代计算每个节点的PageRank值,最终得分最高的句子就是文档的中心句。TextRank的优势是不需要训练语料,对单文档摘要效果稳定。
下面是一个基于TextRank的关键句提取实现,使用了jieba分词和networkx库:
import jieba
import numpy as np
import networkx as nx
from sklearn.feature_extraction.text import CountVectorizer
def textrank_extract(sentences, top_k=3):
# 分词并过滤停用词
stop_words = set(['的', '了', '是', '在', '和', '有', '与', '对', '为', '中'])
tokenized = []
for s in sentences:
words = [w for w in jieba.cut(s) if w not in stop_words and len(w) > 1]
tokenized.append(' '.join(words))
# 构建句子相似度矩阵(词重叠度)
vectorizer = CountVectorizer(tokenizer=lambda x: x.split())
matrix = vectorizer.fit_transform(tokenized).toarray()
sim = np.dot(matrix, matrix.T)
for i in range(len(sentences)):
for j in range(len(sentences)):
if i != j and sim[i][j] > 0:
sim[i][j] = sim[i][j] / (np.log(1 + matrix[i].sum()) + np.log(1 + matrix[j].sum()))
# 迭代计算句子权重
graph = nx.from_numpy_array(sim)
scores = nx.pagerank(graph)
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [sentences[i] for i, _ in ranked[:top_k]]这段代码的关键在于相似度归一化:除以两个句子词数的对数之和,可以防止长句因为词多而获得虚高的相似度,这是原始TextRank论文推荐的做法。提取出的句子需要按它们在原文中的顺序重新排列,否则摘要读起来会跳跃。
用MMR策略控制句间重复
单纯按得分取Top-K句子会有一个问题:得分最高的几个句子往往内容高度相似,因为它们讨论的是同一个主题。最大边际相关(Maximal Marginal Relevance)算法在挑选每个句子时,同时考虑它与查询的相关性和它与已选句子的冗余度,公式为 score = λ * relevance - (1 - λ) * redundancy。λ越接近1,越偏向相关性,越接近0,越偏向多样性,实践中通常取0.7左右。
实现上,维护一个已选句子列表,每轮从候选集中选出使MMR得分最大的句子加入列表,直到达到目标句数。这样可以保证摘要里的每句话都贡献新的信息。对于新闻这类多主题文档,MMR的效果改善尤其明显;而如果原文本身是单主题的论述文,λ可以适当调高,避免过度追求多样性而漏掉核心表述。
句子级压缩:从删减到语义合并
句子压缩的目标是把“据有关部门昨日发布的最新数据显示,今年前三季度我国外贸进出口总值同比增长超过一成”压缩成“前三季度外贸总值同比增长超一成”。实现方式大致分三类。第一类是基于规则的语言学方法:删除时间地点状语、插入语、引导句式,比如去掉“据报道”“值得注意的是”这类零信息量前缀。这类方法精度高但覆盖率有限,适合作为预处理步骤。
第二类是基于句法分析的方法:先做依存句法分析,识别句子的主干(主谓宾)和修饰成分,然后保留主干、删除枝叶。这类方法依赖句法分析器的质量,中文场景下可以使用LTP或HanLP。第三类是数据驱动的方法:用平行语料训练序列标注或编解码模型,让模型自动学习删词决策。近年来基于预训练模型的方法逐渐成为主流,比如用BART、T5这类序列到序列模型直接做生成式压缩,输入长句输出短句,效果明显优于规则方法。
下面是一个基于规则的简单压缩器示例,展示了删除常见冗余成分的思路:
import re
# 零信息量前缀与冗余表达模式
PREFIX_PATTERNS = [
r'^(据了解|据报道|值得一提的是|需要注意的是|众所周知)[,,]?',
r'^(据[^,,]{1,10}报道)[,,]?',
r'^(相关|有关)?(数据|消息)(显示|表明)[,,]?',
]
def compress_sentence(sentence):
result = sentence
for pattern in PREFIX_PATTERNS:
result = re.sub(pattern, '', result)
# 删除句首的时间地点状语,如“昨日,”“今年以来,”
result = re.sub(r'^(今年|去年|近日|昨日|目前)?[,,]', '', result)
# 合并连续标点,清理首尾空白
result = re.sub(r'^[,,、\s]+', '', result).strip()
return result
text = '据相关部门昨日报道,今年前三季度外贸进出口总值同比增长超过一成。'
print(compress_sentence(text))
# 输出:前三季度外贸进出口总值同比增长超过一成规则压缩的优点是可控、无副作用,缺点是无法处理需要语义理解的压缩,比如把“同比增长超过百分之十”改写成“增长超一成”。要做到这一步,就需要借助预训练语言模型,通过微调让模型掌握释义和合并的能力。
抽取式与生成式方案如何选择
抽取式摘要直接从原文选句子,优点是保真度高、不会捏造事实、工程实现简单,缺点是难以跨越句子做信息融合,且对原文写作质量依赖较大。生成式摘要由模型重新组织语言,可以把分散在多处的信息合并成一句话,压缩比和可读性都更好,但存在幻觉风险,即输出原文没有的内容。对于法律文书、财务报告这类对准确性要求极高的场景,抽取式加轻量压缩是更稳妥的选择;对于资讯类App的摘要卡片、搜索结果摘要等容忍度较高的场景,生成式方案能带来更好的用户体验。
一个务实的工程路线是分阶段组合:先用TextRank加MMR完成关键句提取,再用规则方法清理句内冗余,最后视需求决定是否引入生成式模型做进一步改写。每个阶段的输出都可检验、可回退,出问题时容易定位。这种流水线式架构在实际项目中比端到端黑盒模型更容易维护,也方便根据业务反馈持续调优。
总结来说,摘要去冗余不是一个单点算法问题,而是提取、去重、压缩三个环节的协同。先保证选对了句子,再保证句子之间不重复,最后把每个句子榨干水分,摘要的紧凑度和信息密度自然就能上一个台阶。