☰
中文抽取式自动文摘实战:从语料清洗到Rouge评分
2026/10/13 4:18:52 网站建设 项目流程

做中文抽取式自动文摘这个题目,我前后折腾了大概三周,从语料清洗开始一路踩坑到Rouge打分,最后把整个流程完整跑通的时候,最大的感受就是:这个方向看起来简单——不就是“挑几句话拼成摘要”嘛,但真正上手之后你会发现,从数据到方法到评价,每一步都有大量“文档里不会写”的细节。这篇就把整个流程拆开揉碎了讲清楚,包括中文语料怎么处理、三种抽取式方法的具体实现,以及Rouge评价怎么配置、怎么解读分数,希望能给正在做NLP课程项目或入门研究的同学省下一点摸索时间。

先说一句,这里的核心任务是:给一批中文新闻/文档,自动生成一段“从原文中抽取原句”组成的摘要,然后用Rouge指标和参考摘要对比打分。这属于抽取式自动文摘(Extractive Summarization),不是生成式。抽取式的优势在于不需要GPU、不需要大规模训练,纯统计和图算法就能跑出不错的结果,非常适合作为NLP入门的综合实践项目。适合谁来参考?如果你正在做NLP期末项目、毕设的预处理和baseline部分,或者想搞清楚“TextRank到底怎么调参能更好用”,这篇应该能帮到你。

1. 整体设计思路与语料预处理

1.1 中文语料库处理:为什么这里才是第一道坎

很多教程一上来就讲TextRank、讲TF-IDF,但实际项目中,数据的干净程度直接决定后面所有方法的上限。中文语料处理比英文多出了好几个特有的坑:没有天然空格分词、全半角符号混杂、编码不统一、繁体简体混排,还有各种奇怪的网站模板噪声。我第一次用的语料是从公开渠道爬来的新闻数据,共约两万篇,长度从几十字到上万字不等。起初根本没做清洗,直接把原文丢给分词工具,结果生成的摘要里出现了一堆“点击查看”“原标题”“本文来源”之类的模板碎片,Rouge-1分数一度只有0.2左右,完全没法看。

所以第一步,我把语料处理分成三个子步骤:

  1. 编码统一与格式清理:所有文本统一转为UTF-8,剔除乱码字符,将全角字母数字转为半角,去除HTML标签、多余空白和不可见字符。
  2. 正文抽取与噪声过滤:根据DOM结构和文本密度去掉页面模板,过滤掉长度小于50字的“碎片文档”,同时把所有换行符合并为完整的自然段落。
  3. 句子切分与基础清洗:按中文句末标点(。!?……)切分句子,去掉引号和括号里的“来源”“责任编辑”等字段,再做一次低频字符过滤。

这一步我强烈建议单独写一个脚本跑完并保存为中间文件,不要和后面的分析逻辑混在一起。为了可复现,处理完的语料我存成了统一JSON格式,每篇包含id、title、content_sentences(已切分好)、reference_summary(参考摘要)。后面的所有实验都从这个JSON读取,而不是每次重新清洗。

1.2 分词的隐性坑

中文抽取式方法几乎都依赖分词结果。常用基础库分词效果还算稳定,但有几个坑容易踩:

  • 新词和领域词被切碎:比如“人工智能”可能被切成“人工/智能”也就算了,但“碳中和”如果被切成其他碎片,后面的关键词权重计算就会偏掉。解决方法是维护一个自定义词典,把语料里高频出现的二字词到四字词手动加进去。
  • 数字、英文、URL混在一起:分词器会把“2024年”切得很碎,我测试过几种常见库,结果差异不小。处理方式是在分词前先用正则把URL、邮箱、连续数字、版本号等先替换为占位符,分词后再还原,避免这些噪声影响关键词提取。
  • 分词粒度选择:做抽取式文摘时建议用“精准模式”,不建议用“全模式”,因为全模式会产生大量冗余词。另一个心得是:分词后对每个词做词性标注(可以选用带词性标注的分词库),后面抽取句子时可以过滤掉纯虚词、语气词占比过高的句子。

提示:分词的统一性很重要。Rouge评价时如果候选摘要和参考摘要的分词粒度不一致,分数会明显偏低且不公平。我实践下来,所有摘要输出前都要用同一套分词方案处理,避免因为“初步/发展”切法不一致导致失分。

1.3 停用词表不要去网上随便复制一份

停用词表看起来是个小东西,但影响非常大。网上流传的中文停用词表有些过于激进,把“我们”“可以”“其中”这种有实际语义的词也过滤掉了,结果就是关键句识别时错失很多线索。我自己的做法是:先拿一份通用停用词表做基础,然后统计语料中的高频词,人工检查前500个高频词,把明显是噪声的词加进去,同时把“不”“没有”“但”这类带转折/否定语义的词从停用词表里拿掉——因为这些词往往标志着关键信息。

另外,标点符号在中文抽取式任务里也要当作“词”来处理。句首的引号、括号、书名号常常和正文内容强相关,如果清洗得太干净反而会导致句子边界信息丢失。我最后保留了几个重要符号并入句向量中参与计算,而不是简单删光。

2. 三种抽取式自动文摘方法实现

三种方法我分别实现了:TF-IDF权重排序、TextRank图排序、LSA潜在语义分析。三者思路完全不同,对比起来特别能说明问题。

2.1 方法一:TF-IDF权重排序法

这个方法的逻辑非常直观:把每篇文档看成一个句子的集合,每个句子由若干词构成。我们假设重要的句子包含更多重要的词——而“重要词”用TF-IDF来衡量。

TF-IDF的计算分为两部分:

  • 词频(TF):词在句子中出现的次数,可以归一化处理;
  • 逆文档频率(IDF):衡量词的区分度。一个词如果在很多文档中都出现,那它的区分度就低,IDF就小。

对于句子评分,常见做法是:句子得分 = 其中所有词的TF-IDF值之和,再除以句子长度做归一化(避免长句天然占便宜)。

我用的实现要点如下:

import math import jieba from collections import Counter def compute_tfidf(docs_tokenized): """docs_tokenized: list of list of list, 每篇文档是句子的列表,每个句子是词的列表""" df = Counter() total_docs = len(docs_tokenized) doc_freq_counter = Counter() for doc in docs_tokenized: words_in_doc = set() for sent in doc: for w in sent: if w not in words_in_doc: words_in_doc.add(w) for w in words_in_doc: doc_freq_counter[w] += 1 tfidf_dicts = [] for doc in docs_tokenized: doc_tf = Counter() for sent in doc: for w in sent: doc_tf[w] += 1 max_tf = max(doc_tf.values()) if doc_tf else 1 tfidf = {} for w, cnt in doc_tf.items(): tf = cnt / max_tf idf = math.log((total_docs + 1) / (doc_freq_counter[w] + 1)) + 1 tfidf[w] = tf * idf tfidf_dicts.append(tfidf) return tfidf_dicts def score_sentences_tfidf(doc_sentences, tfidf): sent_scores = [] for sent in doc_sentences: score = 0.0 word_cnt = 0 for w in sent: if w in tfidf: score += tfidf[w] word_cnt += 1 sent_scores.append(score / max(word_cnt, 1)) return sent_scores

选句策略上,除了按得分取Top K,我在实践中还加入了两个约束:

  • 位置惩罚:新闻语料的前两句往往是导语,包含高度浓缩的信息,因此给前三句乘以一个1.2的位置加成。
  • 冗余惩罚:如果把排名前3的句子放一起发现重叠词过多(余弦相似度大于0.7),就跳过后面那条,换下一条,避免摘要像复读机。

TF-IDF方法最大的优点是快、可解释性强,但缺点是它假设句子之间彼此独立,忽略了文档内部的语境和句子之间的信息传递。因此对信息分散的长文,效果会明显下降。

2.2 方法二:TextRank图排序法

TextRank本质上就是PageRank算法用在句子上。把每个句子看成图里的一个节点,句子之间的相似度作为边的权重,然后迭代计算每个句子的“重要性得分”。思想和“被越多重要句子指向的句子就越重要”这句话是同一个逻辑。

句间相似度我用的是基于词向量的余弦相似度,公式是:

[ \text{similarity}(S_i, S_j) = \frac{\sum_{w \in S_i \cap S_j} \text{weight}(w)}{\sqrt{|S_i|} \cdot \sqrt{|S_j|}} ]

其中weight(w)可以用词频或TF-IDF值。实现时建议预先计算所有句子对的相似度矩阵,否则每轮迭代都要重算,文档一长会慢到怀疑人生。

核心代码如下:

import numpy as np def build_similarity_matrix(sentences, word_weights): n = len(sentences) sim_mat = np.zeros((n, n)) sent_vecs = [] for sent in sentences: vec = Counter() for w in sent: if w in word_weights: vec[w] = word_weights[w] sent_vecs.append(vec) for i in range(n): for j in range(n): if i == j: continue overlap = set(sent_vecs[i].keys()) & set(sent_vecs[j].keys()) weight_sum = sum(word_weights[w] for w in overlap) norm = (math.sqrt(sum(v * v for v in sent_vecs[i].values())) * math.sqrt(sum(v * v for v in sent_vecs[j].values()))) sim_mat[i][j] = weight_sum / max(norm, 1e-6) return sim_mat def textrank(sim_mat, d=0.85, max_iter=100, tol=1e-4): n = sim_mat.shape[0] scores = np.ones(n) / n for _ in range(max_iter): new_scores = np.ones(n) * (1 - d) / n for i in range(n): for j in range(n): if sim_mat[j][i] > 0: col_sum = sim_mat[j].sum() if col_sum > 0: new_scores[i] += d * scores[j] * sim_mat[j][i] / col_sum if np.abs(new_scores - scores).sum() < tol: break scores = new_scores return scores

调参心得:TextRank里d(阻尼系数)取0.85是经典值,但我在新闻语料上测试发现0.9到0.95之间的表现更好,因为它更强调“互相指向”的图结构传导。另外,相似度阈值也很重要——如果所有句子两两都有边,图会变成完全图,排名就扁平化了。我实际处理中把相似度小于0.2的边直接置为0,经过测试后Rouge-1平均提升了2到3个百分点。

TextRank相比TF-IDF的优势是能捕捉句子间的全局关系,一个句子就算本身关键词不突出,但如果它和很多强句相似,得分也会被抬起来。缺点是计算复杂度高,6000字的文档做全对相似度要几百次余弦计算,实测对长文本优化空间很大。

2.3 方法三:LSA潜在语义分析法

LSA是三种方法里最“数学”的一个,思路很巧妙:先构建“词-句子”矩阵,矩阵元素是词在句子中的TF-IDF权重;然后对这个矩阵做奇异值分解(SVD),把它压缩到一个低维潜在语义空间;最后根据句子在“主题方向”上的投影长度来打分。

这里的关键理解是:SVD把原始高维稀疏矩阵分解成三个矩阵的乘积,中间的奇异值矩阵对角线上是递减的奇异值。前k个奇异值对应的左右奇异向量就可以理解为k个“潜在主题”。句子在某个主题上的权重就是右奇异向量中对应的数值。对于抽取任务,我取前k个主题的加权平方和作为句子得分,权重是每个主题对应的奇异值的平方。

from sklearn.decomposition import TruncatedSVD def lsa_summarize(term_sent_matrix, k=5): svd = TruncatedSVD(n_components=k, random_state=42) reduced = svd.fit_transform(term_sent_matrix.T) # 句子-主题矩阵 # 每句得分 = 各主题权重的加权平方和 scores = np.sum(np.abs(reduced) ** 2, axis=1) return np.argsort(scores)[::-1]

LSA方法对同义词和多义词的处理在三种方法里最好,因为它不是直接做词汇匹配,而是通过主题空间把语义相近的句子拉近。我实测在科技类新闻上,LSA的Rouge-2(二元词组重叠)比TextRank高出约5%,原因就是它能抓住“模型”“算法”“训练”这类同义表达在主题空间中的相似性。

但LSA的缺点也很典型:需要调k值,k太小会丢失信息,太大会保留噪声。我通常根据语料平均句子数设定k在5到15之间,再做一个简单网格搜索,用验证集的Rouge分数选最优k。另外SVD在矩阵比较大时内存和耗时都不小,建议用稀疏矩阵存储。

2.4 三种方法横向对比与融合尝试

我做了一组对照实验,语料是随机选的500篇新闻,每篇人工写参考摘要,用Rouge-L作为主指标,结果大致如下:

方法Rouge-1 (F1)Rouge-2 (F1)Rouge-L (F1)平均单篇耗时
TF-IDF0.3650.1390.3250.3秒
TextRank0.4020.1580.3572.8秒
LSA0.3910.1510.3491.2秒
三种融合(平均分排序)0.4210.1710.3794.5秒

融合的方式我试了两种:一种是直接把三种方法的句子得分做归一化后平均,按总分选句;另一种是每个方法各选Top 5句子再做最大边缘相关去重。结果显示第一种更稳,因为各方法的错误模式差异比较大,平均能相互抵消一部分噪声。融合之后虽然耗时增加了,但Rouge-L提升大约2到3个点,如果你对速度不太敏感,值得一试。

3. Rouge评价方法:安装、使用与打分细节

3.1 Rouge原理简述

Rouge(Recall-Oriented Understudy for Gisting Evaluation)是自动文摘领域最常用的评价指标,核心思想是:把生成的摘要和参考摘要做n-gram重叠度统计。最常用的几个变体:

  • Rouge-1:单字/词级别的重叠,衡量“内容点有没有覆盖到”;
  • Rouge-2:相邻二元词组(bigram)的重叠,衡量“短语搭配和流畅度”;
  • Rouge-L:基于最长公共子序列(LCS),同时考虑顺序和长度,比Rouge-1/2更接近人对“摘要是否连贯”的判断。

Rouge本质上以召回率为导向,所以它特别关注“参考摘要中的词有没有出现在生成摘要里”,但对生成摘要中多余的内容惩罚相对较小。这一点在做中文摘要时一定要注意,因为中文里加一堆废话会拉低准确率但召回率可能不变,导致分数“虚高”。

Rouge具体计算时,摘要需要按空格分开的词序列。中文必须预先分词,否则Rouge会把整句当成一个“词”,分数完全没有意义。

3.2 环境配置与命令细节

我最早尝试直接用Python的rouge库来算,发现安装很简单,但2023年之后依赖的旧包在新环境里容易出现兼容问题,还会因为字符编码踩坑。后来我换成了files2rouge加官方标准脚本的方案,整体流程如下:

  1. 准备三个目录:candidate/(生成的摘要)、reference/(参考摘要)、output/(得分结果)。
  2. 两个目录下的文件必须一一对应,文件名一样,且所有摘要统一为UTF-8编码。
  3. 所有候选摘要和参考摘要先进行一次严格的“分词+空格拼接”,分词器和词表要保持完全一致。
  4. 运行评估命令:
files2rouge candidate/ reference/ --output_file output/scores.txt

这个命令会自动调用Rouge标准脚本计算Rouge-1、Rouge-2、Rouge-L等指标。整个过程需要在本地安装Perl并确保相应环境变量配置无误。如果不想折腾环境,也可以直接用Python的rouge-score库做轻量级计算,但分数字段和官方脚本略有差异,写论文或做实验对比时要确保全流程统一。

3.3 打分数值解读与对齐分歧

Rouge分数看几个维度就够了:Rouge-1的F1衡量整体信息覆盖率,Rouge-2衡量局部搭配质量,Rouge-L衡量结构顺序。一套实际跑出来的典型结果可能长这样(单篇示例):

  • Rouge-1: 0.431
  • Rouge-2: 0.187
  • Rouge-L: 0.382

这三个数值之间差距较大是正常的,因为二元词组匹配比单字匹配苛刻得多。如果Rouge-2特别低,通常是分词结果不一致、系统摘要过度使用同义改写,或者摘要句子顺序与参考摘要错位导致的。

另外,参考摘要写得好不好会直接影响分数上限。有些参考摘要本身就是人工高度概括的句子,根本不是从原文直接抽取的,这时抽取式系统的Rouge分数天然偏低——这是任务设定问题,不是你的方法问题。我后来改用“先人工从原文标出3句关键句,再做轻度改写”的方式生成参考摘要,模型的Rouge-L直接从0.3提升到0.38以上,所以参考摘要的设计要和任务定位对齐。

3.4 实验设计建议

在做Rouge对比时,记得固定随机种子,保证三种方法在同一文档子集上测试,并且所有摘要长度统一(比如都限制在原文的30%以内但不超过80字)。如果不统一摘要长度,长摘要会天然获得更高的Rouge分数,导致对比失真。我试过把这个长度约束写进选句循环,用贪心方式逐步增加句子直到长度上限,最后得到的分数才具备可比性。

另一个细节:多参考摘要(每篇文档配2到3份人工摘要)会让Rouge分数更有说服力,因为它能抵消单个人工摘要的措辞偏差。如果人力不足,至少两篇是底线。

4. 常见问题与排查技巧实录

4.1 分词不一致导致的分数虚高或偏低

这是最常见也最隐蔽的问题。比如候选摘要分词为“中国/经济/持续/发展”,参考摘要却是“中国/经济/持续发展”,Rouge-2会漏匹配“经济发展”这个二元组,分数直接掉了一个档次。我采用的做法是:所有摘要文件在交给Rouge之前统一过一次同一分词器,并把停用词过滤策略也保持一致。如果要对齐学术界常见做法,建议在论文中明确写明分词工具与词表版本,保证可复现。

4.2 输出空摘要或长度过短

TextRank和LSA在极短文本(少于5句)上经常出现句子得分都差不多、选出来的摘要不满50字的情况。原因是短文本的句子间相似度矩阵太密集或太稀疏,迭代收敛后分数差异极小。我给这两种方法都加了一个兜底逻辑:如果最终摘要长度低于目标长度的50%,就退回用第一句加第二句(新闻语料里通常是导语)拼摘要。这个方法在实验中挽救了大约6%的案例。

4.3 编码与换行符问题

Windows环境下写完的文件被Rouge脚本读取时,有时会因为UTF-8 BOM头导致第一句开头多出一个看不见的字符,Rouge-1分数字面上看挺正常,但Rouge-L会小幅下降。处理方式是清洗所有文件为UTF-8无BOM格式,换行符统一\n,避免\r\n混用。这个坑我踩了整整一个下午,最后是用十六进制编辑器看文件头才发现。

4.4 语料偏差与摘要位置偏好

如果你的语料都来自某个特定领域,比如全是娱乐新闻,那么标题句位置的特征会被模型学得过重,换到新闻语料上效果会崩掉。我做交叉验证时发现,文本开头与结尾句被选中的概率显著高于中间句子——这是位置偏置。解决方法是做句子位置特征融合时设置一个最大加成上限,避免位置权重过大。通用经验是位置加权的系数不要超过1.3,否则模型基本退化成“直接取前几句”。

4.5 参数调优的实用建议

三种方法都有的关键参数是摘要长度系数、停用词表大小和冗余阈值。我的建议是:先用小规模开发集跑一轮全参数组合,把最终选句的平均长度和参考摘要长度画出来看是否匹配。TextRank还要关注阻尼系数和迭代次数;LSA要关注主题数k。每次跑参数组合的时候把所有随机种子固定,记录下每个参数组合的Rouge-1/2/L,存成CSV,最后用脚本来选最优,而不是靠肉眼比较命令行的零散输出。

5. 最后想说的一点经验

整套项目做下来,我最深的体会是:自动文摘项目成败的关键往往不在算法本身,而在数据质量和评价流程的严谨程度。同样的TextRank实现,用脏数据和干净语料跑,Rouge-L能差出0.1以上——这在很多论文里就是“显著优于基线”和“结果不显著”的天壤之别。

所以如果你正在跑类似的项目,建议把时间分配调成:40%做语料清洗和参考摘要整理,40%做Rouge评价调试,只有20%留给算法本身。算法再花哨,如果评价环节不严谨,结果依然站不住脚。反过来,数据干净、评价扎实的情况下,哪怕只用TF-IDF加TextRank做融合,也能写出一篇逻辑完整的实验报告或一篇靠谱的课程项目。

最后分享一个小技巧:在做完所有实验之后,不要把生成的摘要直接扔掉,挑几篇分数高和分数低的案例,把候选摘要、参考摘要、得分明细放在同一个表格里对比查看。这个习惯帮我把Rouge分数和真实质量之间的对应关系建立起来,后续调参都更有方向感。希望这篇对正在做中文抽取式文摘的你有点帮助。

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

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

立即咨询