☰
深度学习驱动的文本摘要自动生成:完整工程实践与避坑指南
2026/10/16 3:19:31 网站建设 项目流程

简介:面向本科毕业设计与自然语言处理入门者的深度学习文本摘要项目,聚焦用Transformer模型实现长文关键信息提取与摘要生成。资源共34个文件,压缩后约360KB,其中18个Python脚本覆盖数据预处理、模型构建、训练、评估与Web演示等环节,5个Shell脚本提供一键式训练、测试及CI入口,另有YAML配置、词汇表、Dockerfile与README,便于快速复现环境并了解目录结构。项目遵循经典编码器-解码器架构,代码中完整呈现分词、词汇表构建、序列化输入、损失函数和优化器配置,以及基于ROUGE与BLEU的摘要质量评估;同时包含beam search解码、训练日志管理等工程化细节。已有3718人学习下载,既能直接支撑本科论文实验,也能帮助初学者系统掌握Transformer在文本摘要任务上的落地流程,从环境配置到结果评估均有章可循。

1. 基于深度学习的文本摘要自动生成:一个把长文本压成几句通顺话的完整工程

你可能也遇到过这种情况:一篇三千字的新闻报道,人工读完要五分钟,摘出重点再写一段摘要,又是五分钟。文本摘要自动生成这个任务就是解决这个问题的,它基于深度学习和自然语言处理技术,用 Transformer 系列模型把长文压缩成携带核心信息的短句。我第一次跑通这个项目时印象很深:输入一篇体育新闻,模型生成的摘要居然能准确抓住比分、进球时间和关键球员,而不是简单复制开头几句——那一刻我才确定这套流程真的可用。

这个项目的价值在于它不是单点模型演示,而是把数据预处理、模型微调、解码生成、自动评估串成了一条完整的落地方案,很适合做本科毕业设计的工程底座。无论你是想快速搭出一个能演示的摘要系统,还是想深入理解生成式文本任务的数据流和训练细节,都可以直接基于它改。

2. 数据与预处理:从原始新闻语料到模型能读的样本对

2.1 数据集形态与摘要任务的定义

文本摘要天然分成抽取式和生成式两类。抽取式从原文里挑句子拼成摘要;生成式则让模型自己重新组织语言,这套项目走的是生成式路线。对应的训练数据和机器翻译很像:一组组「原文—摘要」配对。一篇新闻文章的正文是输入,配对的新闻要点就是模型要预测的目标输出。

我见过的本科毕设里,最稳妥的起步数据集是公开的英文新闻摘要语料,数据格式非常干净,每一行包含一个文章的正文和对应的参考摘要。英文语料的好处是分词规则简单,预训练模型覆盖充分,适合先把流程跑通再换领域。中文场景下常见做法是使用公开的短新闻摘要数据集,或者自己爬取新闻正文并把标题当作弱摘要,但需要注意清洗成本。

2.2 文本清洗与长度截断策略

原始语料里通常有大量网页残留符号、多余空白和重复的标点,这些内容会干扰模型学习。清洗阶段我一般会做这几件事:把 HTML 实体还原成正常字符,统一英文大小写,压缩连续空格,过滤掉长度过短或过长的异常样本。长度过滤尤其重要,因为 Transformer 的自注意力复杂度是输入长度的平方,一篇五千词的新闻直接塞进 BERT 类模型会非常吃力。

处理长文本的通用方案是截断。输入侧限长通常设为 512 个 token,因为 BART 这类预训练模型的标准输入窗口就是 512;输出侧摘要限长设为 128 个 token。这里有一个容易被忽略的点:原文是随机从头部截断,还是在开头保留、在结尾抽样?对于新闻语料,导语往往在最前面,所以从头部直接截断反而能保留关键信息。我在预处理时还会把截断前的长度分布打出来看一眼,如果大部分文档都集中在 400 词上下,说明语料偏短,截断策略可以更激进。

def preprocess_function(batch): # 输入侧:保留原文前 4000 个字符,再由 tokenizer 截断到 512 个 token articles = [doc[:4000] for doc in batch["article"]] model_inputs = tokenizer( articles, max_length=512, truncation=True, padding="max_length", return_tensors="pt", ) # 输出侧:参考摘要截断到 128 labels = tokenizer( batch["highlights"], max_length=128, truncation=True, padding="max_length", return_tensors="pt", ) model_inputs["labels"] = labels["input_ids"] return model_inputs

这段代码里有几个参数值得说清楚。truncation=True表示超过max_length的部分直接丢弃,否则有些超长样本会报错;padding="max_length"会把短样本统一补到同样长度,这样能拼成规整的 batch 张量,但代价是增加无效计算,如果数据长短差异大,更推荐改成padding=True做动态填充。labels就是训练时的目标序列,后续损失计算会拿它和模型输出的 logits 做交叉熵。

2.3 标签掩码与 DataLoader 的构建细节

上一段把摘要填充到了 128 个 token,但填充位置上的<pad>标记不能参与损失计算。处理方式是把 labels 中对应 padding 位置改成-100,PyTorch 的交叉熵损失函数遇到-100会自动跳过该位置。这个细节不处理的话,模型会把大量算力花在预测「空白」上,训练出来的模型会倾向于生成更短的摘要。

另一个容易忽略的点是 sequence 长度对齐。seq2seq 模型通常要求 labels 比 encoder 输入短一个 token,因为训练时做 teacher forcing,每个位置预测下一个 token,最后一个 token 没有后继目标。用 Hugging Face 的 Trainer 时它会自动处理偏移,但如果是自己写训练循环,就必须手动移位:把 labels 往左移一位作为 decoder input,原 labels 作为预测目标。

from torch.utils.data import DataLoader dataset = encoded_dataset.map(preprocess_function, batched=True) dataset.set_format(type="torch", columns=["input_ids", "attention_mask", "labels"]) dataloader = DataLoader( dataset, batch_size=8, shuffle=True, collate_fn=lambda batch: { "input_ids": torch.stack([x["input_ids"] for x in batch]), "attention_mask": torch.stack([x["attention_mask"] for x in batch]), "labels": torch.stack([x["labels"] for x in batch]), }, )

这里的collate_fn作用是把一批样本堆叠成张量。如果不用自定义函数,Hugging Face 的默认 DataCollator 会重新做 padding,但前提是数据集里没有预先 pad 到固定长度。两种思路都可以,关键是保持 labels 和 input_ids 的 pad 标记一致。训练时建议先取一个小 batch 跑一遍前向,确认 loss 能正常下降,再看训练曲线的整体走势。

3. 核心模块拆解:Decoder 如何从原文逐步生成摘要

3.1 整体架构:Encoder-Decoder 与预训练模型的选择

这套项目基于 Transformer 的 Encoder-Decoder 架构。Encoder 负责把原文编码成语义向量,Decoder 负责逐个 token 地生成摘要。纯 BERT 这类 Encoder-only 模型做分类很擅长,但生成任务需要 Decoder 结构,所以常见的落地做法是选用 BART 或 T5 这类预训练 Seq2Seq 模型。它们在预训练阶段就见过大量「文本破坏—重建」任务,对摘要生成这种压缩重写的任务有很好的起点。

我个人更偏好 BART 作为默认选择。它处理长文本的稳定性好,生成质量在新闻摘要上表现均衡,而且 400M 参数量级在一张消费级显卡上就能微调。T5 的优势是任务统一,但中文场景如果要用 T5 需要额外找中文预训练权重。无论选哪个,项目里真正需要自己写的不是模型结构,而是训练策略和解码参数,这是最容易拉开效果差距的部分。

3.2 解码生成:Beam Search 与重复惩罚

微调完成后的推理阶段,模型需要一个解码策略来逐词生成摘要。最简单的贪心解码每步选概率最高的 token,但容易陷入局部最优,生成平淡甚至重复的文本。更常见的是 Beam Search——每一步保留概率最高的 K 个候选序列,最后从 K 条候选里挑整体得分最高的一条。K 一般取 4 或 5,太大生成变慢,太小效果接近贪心。

推理代码用 Hugging Face 的model.generate一行就能跑起来,真正的功夫在参数调优上。no_repeat_ngram_size=3表示连续三个词不重复出现,能大幅抑制生成阶段常见的重复片段问题;length_penalty控制生成长度偏好,负值鼓励短句,正值鼓励长句。对新闻摘要,length_penalty=2.0左右能让摘要覆盖更多信息点,而不是只抓开头一句。

from transformers import AutoTokenizer, AutoModelForSeq2SeqLM tokenizer = AutoTokenizer.from_pretrained("./summary-model") model = AutoModelForSeq2SeqLM.from_pretrained("./summary-model") inputs = tokenizer( long_text, return_tensors="pt", max_length=512, truncation=True, ) summary_ids = model.generate( **inputs, num_beams=4, max_length=128, min_length=30, no_repeat_ngram_size=3, length_penalty=2.0, early_stopping=True, ) summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True) print(summary)

参数min_length=30是为了防止模型生成过短的摘要,尤其当输入文本质量不高时,模型倾向于偷懒只生成半句话。early_stopping=True表示当所有候选序列都生成了结束符就提前终止。这几个参数是解码阶段最值得反复调的组合,因为它们直接决定了生成摘要的通顺度和信息密度。

3.3 训练时的监督信号:Teacher Forcing 与损失计算

训练生成式摘要和推理有一个关键差异:训练时不会让 Decoder 自己一步步往下猜,而是把参考摘要的真实 token 作为每一步的输入,让模型预测下一个 token。这叫 Teacher Forcing。它的好处是训练稳定、收敛快,但代价是训练和推理存在分布偏差——训练时看到的都是真实历史,推理时输入的是自己生成的内容,模型表现会打折扣。

具体到代码实现,Hugging Face 的模型在前向时会自动做这一步:传给模型的labels会被内部处理成 decoder input,并在计算损失时对每个位置预测下一个 token 的 logits 和真实 token 做交叉熵。所以训练代码里最关键的是保证labels的正确性,尤其是 padding 位置要设成-100。我见过不少第一次做生成式任务的同学在这里翻车,症状是 loss 一直不降或模型只会生成<pad>,排查下来都是 labels 里的填充符号参与了损失计算。

4. 训练与评估:Loss 曲线、ROUGE 分数与模型落地

4.1 训练主循环与超参数配置

微调阶段用 Hugging Face 的Seq2SeqTrainer是最省事的路径,它把梯度累积、学习率调度、评估、断点保存都帮你接好了。比较关键的超参数是学习率和 batch size。基于预训练模型微调,学习率建议在2e-5到5e-5之间,太大容易破坏预训练权重,太小收敛极慢。batch size 主要受显存限制,8 的 batch 配上梯度累积也能达到不错的效果。

一个值得注意的坑是predict_with_generate=True这个开关。它控制训练过程中的评估阶段是否用生成方式计算指标。如果不开,评估阶段只算语言模型的困惑度,和最终的 ROUGE 相关性弱;开了之后,每个 epoch 结束时会跑一遍 beam search 生成摘要再算指标,更接近最终使用场景,但耗时也会明显增加。

from transformers import Seq2SeqTrainer, Seq2SeqTrainingArguments training_args = Seq2SeqTrainingArguments( output_dir="./results", num_train_epochs=3, per_device_train_batch_size=8, learning_rate=3e-5, warmup_steps=500, weight_decay=0.01, predict_with_generate=True, evaluation_strategy="epoch", save_strategy="epoch", logging_steps=100, generation_max_length=128, generation_num_beams=4, report_to="tensorboard", )

warmup_steps=500让学习率在训练前 500 步从 0 线性升到目标值,能缓解大学习率在训练初期的震荡。weight_decay=0.01是 AdamW 优化器的权重衰减,对防止过拟合有一些帮助。save_strategy="epoch"表示每个 epoch 结束保存一次模型权重,如果训练中途崩溃,可以从上一个 epoch 的断点恢复,不用从头再跑。这几乎是所有长训练任务的标配设置。

4.2 ROUGE 指标:摘要任务的核心评价标准

文本摘要的评价指标和分类任务完全不同。分类看准确率,摘要则看生成内容和参考摘要的 n-gram 重叠程度,最常用的是 ROUGE 系列。ROUGE-1 比较单个词的召回,ROUGE-2 比较相邻词对,ROUGE-L 则基于最长公共子序列计算,它更能反映句子的流畅度和结构相似度。

在实现上,推荐直接用evaluate库提供的rouge评估器,注意新版库和旧版load_metric的兼容性问题。计算时需要把模型生成的摘要和参考摘要都转成纯文本,并且做统一的预处理:去掉多余空格、统一大小写。ROUGE 对换行符和标点比较敏感,预处理不一致会导致分数虚低。第一次跑评估时建议先打印几组生成结果和参考摘要的人工对比,确认指标和主观感受一致后再依赖 ROUGE 调参。

from evaluate import load rouge = load("rouge") predictions = ["the quick brown fox jumps over the lazy dog"] references = [["a quick brown fox leaps above a lazy dog"]] results = rouge.compute( predictions=predictions, references=references, ) print(results)

这里references是列表的列表,因为一条生成结果可能对应多条参考摘要,计算时会取所有参考中的最高分。评估生成式模型时,我一般同时打印 ROUGE-1、ROUGE-2、ROUGE-L 三个分数,并且额外看一眼生成摘要的平均长度。如果一个模型的 ROUGE-L 很高但平均长度只有 20 词,说明它学会了用短句绕开长尾信息,参考摘要通常更长、信息更密集,只看单一指标很容易被误导。

4.3 模型保存与推理接口的组织方式

训练完成后,模型保存和推理调用建议走统一路径。Hugging Face 的save_pretrained会把权重、配置、tokenizer 一起存到目录里,加载时只需要AutoModelForSeq2SeqLM.from_pretrained一个接口。我习惯把推理逻辑封装成一个独立的类,输入一段长文本,输出摘要,内部自动完成截断、生成、解码三个步骤。

from pathlib import Path from transformers import AutoTokenizer, AutoModelForSeq2SeqLM class TextSummarizer: def __init__(self, model_dir: str): self.tokenizer = AutoTokenizer.from_pretrained(model_dir) self.model = AutoModelForSeq2SeqLM.from_pretrained(model_dir) def summarize(self, text: str, max_len: int = 128) -> str: inputs = self.tokenizer( text, return_tensors="pt", max_length=512, truncation=True, ) summary_ids = self.model.generate( **inputs, num_beams=4, max_length=max_len, min_length=30, no_repeat_ngram_size=3, length_penalty=2.0, early_stopping=True, ) return self.tokenizer.decode(summary_ids[0], skip_special_tokens=True)

封装成类的好处是后续接 Web 接口或批量评测时,业务代码不需要关心模型细节。model_dir指向保存权重的目录,加载时会自动读取配置和 vocab 文件。如果要把这个类用在不同语言场景,只需要换 tokenizer 和对应的模型权重,调用方式保持不变。

5. 避坑指南:训练摘要模型最容易出现的五个问题

5.1 摘要成了原文复读机

  • 现象:生成结果几乎就是把原文前两三句照搬,没有语言的重新组织。
  • 原因:新闻语料的导语本来就在开头,模型学到的是「抄开头」这种捷径,加上摘要训练数据不足时更容易走偏。另外一个常见诱因是解码参数里没设置重复惩罚,导致模型倾向于复制高概率 token。
  • 解决:先确认参考摘要是否真的经过了人工改写,如果参考摘要本身就是原文开头句,换数据才是根本方案。解码侧把no_repeat_ngram_size设置为 3,并把length_penalty调高到 2.0 左右;另外检查训练时的截断策略,如果原文本开头包含了完整导语,需要对输入做随机截断,避免模型只依赖开头。

5.2 ROUGE-L 分数卡在 0.25 上不去

  • 现象:训练 loss 已经收敛,生成的摘要人工看也通顺,但 ROUGE-L 就是不到 0.3,网上公开论文的分数都在 0.35 以上。
  • 原因:最大可能是评估阶段和训练阶段的数据预处理不一致。比如训练时过滤掉了标点,评估时却带着标点算 n-gram;或者 tokenizer 版本换了导致分词结果不同。另一个原因是测试集的文本长度分布和训练集差距过大,生成的长度上限不够覆盖长文内容。
  • 解决:把训练和评估两套流程里的文本清洗逻辑抽成同一个函数,确保两边对字符串的处理完全一致。测试时把max_length分别设成 64、128、256 跑三组 ROUGE,看分数随长度上限的变化趋势,找到最优区间。如果分数仍然偏低,先把参考摘要单独打印出来对比一下,确认不是数据集本身有问题。

5.3 训练时显存不足,batch size 调到 2 都跑不了

  • 现象:训练脚本一开始就报 CUDA out of memory,或者在训练过程中随机崩溃。
  • 原因:Transformer 的显存占用和序列长度直接相关,512 个 token 的输入加上 128 个 token 的输出,激活值会占据大量显存。batch size 减到 2 仍然爆显存时,问题通常出在梯度累积和混合精度没有正确开启。
  • 解决:优先开启fp16=True,半精度训练能直接省一半显存。然后考虑梯度累积,per_device_train_batch_size=1配合gradient_accumulation_steps=8等效于 batch size 8 的效果。再不行就把输入截断长度从 512 降到 384,牺牲少量信息换取训练可行性。

5.4 生成结果里出现大量重复片段

  • 现象:生成的摘要可能前一句和后一句内容重叠,甚至同一句话连续出现两三遍,看起来像个死循环。
  • 原因:语言模型在解码时容易陷入重复循环,因为重复某个 high-probability 片段会持续获得较高分数,beam search 会放大这种效应。尤其当输入文本较短、信息不足时,模型只能用重复内容凑长度。
  • 解决:首要解法就是no_repeat_ngram_size=3,它会禁止任意三个 token 的连续组合重复出现。如果仍然重复,可以启用repetition_penalty=1.2,对重复 token 的概率做动态惩罚。注意惩罚系数超过 1.5 会导致摘要变得生硬不连贯,需要在这个范围内做细调。

5.5 中文语料跑英文预训练模型,生成乱码

  • 现象:用 BART 英文权重直接训练中文摘要数据,生成的文本包含大量 unicode 替换符和无法辨认的字符。
  • 原因:英文 tokenizer 的 vocab 里根本没有中文字符,中文输入会被切成奇怪的碎片甚至映射到未知 token。这不是模型训练能解决的,而是 tokenizer 层就出了问题。
  • 解决:中文场景必须替换成中文预训练模型。常见做法是用中文 BART 或中文 T5 版本,或者用基于中文语料训练的序列到序列模型。切换后注意检查 tokenizer 的词表大小和特殊 token 定义,有些中文模型使用不同的 padding token 名称,需要在训练前统一。

6. 二次开发:把英文摘要系统改造成中文领域项目的三个切入点

6.1 切换到中文数据集与中文预训练权重

从英文切中文不是换一个 tokenizer 就完事。中文没有天然的空格分词边界,BPE 类 tokenizer 会按字符切分或按字节级组合切分,同一句话的分词结果可能和英文完全不同。建议直接用针对中文语料预训练的 Seq2Seq 权重,训练数据选择公开的中文短新闻摘要集,或自己构造「新闻正文—标题」配对。注意中文摘要任务对标点更敏感,预处理阶段要把全角半角统一,并且保留逗号句号,因为 ROUGE 计算时这些标点会影响连续子串的匹配结果。

6.2 从生成式扩展到抽取式基线对比

生成式模型效果不好时,可以加一个抽取式基线做对照。抽取式用 TextRank 或基于句向量相似度选择原文中最有代表性的句子,拼接成摘要。它的优点是永远不会出现事实错误,缺点是句子不流畅。这个基线可以作为项目里的效果下限,如果生成式模型连抽取式都打不过,说明训练数据或解码参数还有问题。代码上实现一个 TextRank 只需要几十行 Python,可以放在项目的主评估脚本里一并输出对比分数。

6.3 接一个 Web 演示接口

毕设答辩最常见的需求是现场演示。把封装好的TextSummarizer接一个轻量 Web 框架,暴露一个 POST 接口,前端提交文本返回摘要。注意请求体大小限制要调大,因为输入长文本可能超过默认的 1MB 限制;另外模型加载应该放在进程启动时只做一次,不要每次请求都重新加载权重。演示环境没有 GPU 时也能跑,CPU 上 512 token 的输入生成 128 token 的摘要大约需要几秒,交互上可以接受。

我做这类生成式项目有个习惯:微调开始前,先用测试集里的前 20 条样本把完整推理链路跑通,确认生成的摘要肉眼可读、指标能正常计算,再启动正式训练。这个习惯帮我规避了好多次「训练了一天,评估脚本报错」的翻车现场。如果你也打算基于这套项目往下走,我建议把评估脚本和训练脚本写进同一个启动命令里,训练完自动评估、自动保存指标表,就不会出现权重和指标对不上的情况了。希望帮到你。

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

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

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

立即咨询