ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深度学习文本摘要自动生成实战:从TextRank基线到BART微调

深度学习文本摘要自动生成实战:从TextRank基线到BART微调 简介面向本科毕业设计的深度学习和自然语言处理文本摘要项目聚焦Transformer模型在自动摘要任务中的落地实现。压缩包共34个文件含18个Python脚本、5个Shell脚本、3个TXT说明、2个YML配置以及vocab词表、Dockerfile、README等整体仅360KB结构紧凑覆盖数据处理、模型构建、训练、评估、网页演示与容器化部署全流程。其中model.py实现Transformer编码器-解码器结构train.py与train_rl.sh支持标准训练和强化学习训练beam_search.py用于解码生成eval.py结合ROUGE、BLEU等指标评估摘要质量。已有3451人学习下载适合需要快速搭建文本摘要实验环境的本科生或NLP入门者可直接参考代码组织方式并在此基础上进行改进与论文写作。 本科毕业设计选这个题目的人不少但真正能从头把它做好的人不多。基于深度学习的文本摘要自动生成放在自然语言处理领域里算是入门门槛和完成度最容易失衡的方向之一听起来是“让模型总结一篇长文”做起来却要串起分词、编码、序列生成、评估指标一整条链路。我去年完整做了这个项目从数据清洗、基线模型一路做到BART微调这篇文章就把完整流程拆开讲。不管你是正准备开题的本科生还是刚开始接触NLP的读者按这套思路走下来至少能少折腾一个月。1. 项目定位文本摘要任务到底在解决什么问题1.1 摘要任务的两大分支与毕设定位文本摘要的核心目标是给定一篇文档让系统产出一段语义完整、信息密度高的短文本。按实现方式可以分成两大分支抽取式摘要和生成式摘要。抽取式是从原文里挑出若干关键句子直接拼接本质上是一种排序筛选的问题生成式则是让模型在理解全文后像人一样重新组织语言写出一段原文里未必存在的句子。做本科毕设通常建议两条腿走路先用传统抽取式方法做基线再用深度学习做生成式这样既能把两套方案对比起来分析工作量和技术深度也撑得起一篇论文。另外还有一个容易被忽略的分类维度单文档摘要和多文档摘要。毕设基本都做单文档即可也就是“一篇文章对应一段摘要”。多文档摘要需要做跨文档的信息融合对数据规模和模型能力要求高得多本科阶段不建议碰。语言上尽量选中文因为中文新闻语料相对好找也方便在答辩时做效果展示如果英文能力够硬CNN/DailyMail数据集也很经典。1.2 为什么先实现TextRank基线再上深度学习模型很多同学一上来就想直接调预训练模型我建议先停下来。深度学习模型效果再好如果缺少对比说服力会大打折扣。TextRank就是最合适的基线它借鉴了PageRank的思路把每个句子当成图里的节点句子之间的相似度作为边权然后通过迭代计算每个句子的“重要性分数”最后按分数从高到低抽出若干句子组成摘要。整个算法无监督不需要训练数据几十行代码就能实现。关键点在于这个基线必须做得认真不能随便跑个demo糊弄过去。我当时的做法是先用jieba分词再用BM25或余弦相似度计算句间权重然后按重要性排序输出TopK句子为了凑够摘要长度还做了“去冗余”处理防止抽出来的几段话讲的是同一个意思。做完之后你会在测试集上拿到一组指标比如ROUGE-1在0.25左右。后面所有深度学习模型的提升都要用这组数字来衬托所以基线的每一分都要抠清楚。TextRank的局限也很明显它生成不出原文之外的新词抽出来的句子衔接生硬而且对“指代”和“逻辑关系”几乎无感。但这些短板恰恰是深度生成模型的长处所以这个对比本身就是毕设里一个很好的分析点。2. 核心模型选型为什么我最终选了BART2.1 生成式摘要主流模型横向对比决定上深度学习后第一个绕不开的问题就是用哪个模型。当时我列了一张对比表把主流方案过了一遍。模型结构类型是否适合摘要生成中文预训练权重实际体验Seq2Seq AttentionRNN/LSTM编码解码一般少可以作为教学示例训练慢长文本信息丢失严重TransformerEncoder-Decoder适合少并行训练效果好但需要大量数据和算力BERTEncoder-only只适合抽取式多不适合自由生成因为结构上没有生成器GPTDecoder-only可以生成但可控性差相对少开放式生成强做摘要容易跑题BARTEncoder-Decoder很合适有成熟权重预训练任务和摘要天然契合T5/mT5Encoder-Decoder很合适有但体积大统一文本到文本框架效果优秀微调成本略高我最后选了BART核心原因有四个。第一它的Encoder-Decoder结构天生就是“读全文、写摘要”的骨架第二预训练目标和摘要任务高度一致后面细说第三HuggingFace上有可以直接用的中文权重fnlp/bart-base-chinese省去了从零训练的时间第四显存要求对本科机器相对友好base级别模型用一块8G显存的卡就能跑起来。2.2 BART的预训练原理与中文场景适配BART是2019年由Facebook提出的预训练模型它的做法非常有意思预训练时把正常文本“弄坏”比如随机遮挡几个词、删掉一段话、把句子顺序打乱然后让模型把原文还原出来。你可以把它理解成一个“文本修补匠”它被迫学会了理解上下文和恢复语义。正因为预训练阶段练的是这种“从受损输入还原完整输出”的能力微调时把它接到摘要任务上就特别自然输入是完整长文输出是浓缩摘要本质上也是一个“变换”任务。中文场景下直接用fnlp/bart-base-chinese这个权重即可它是用中文语料继续预训练过的BART-base。需要注意BART-base的参数规模大约1.4亿不是很大但效果和算力消耗的性价比非常合适本科毕设。如果你设备很好也可以尝试thu-coai一些更大规模的中文生成模型但我个人建议先把小模型完整跑通后面再谈升级。2.3 动手前必须搞懂的4个基础概念如果你前期没有系统学过深度学习直接调模型很容易一头雾水。我建议先把下面这4个概念弄清楚不要求完全推导公式但至少得知道它们在做什么。Attention机制本质是“给每个词分配注意力权重”。做摘要时模型生成某个词时需要注意原文里的哪些词这就是Attention在做的事。通俗点说就像人写概括时会重点回顾原文里的关键位置。Transformer的Encoder和DecoderEncoder把输入句子编码成一系列向量Decoder根据这些向量逐词生成输出。BART就是这个结构的完整实现。很多同学只学过BERT只有Encoder或GPT只有Decoder遇到BART会有点懵其实它就是两者结合。Beam Search生成文本时最简单的策略是贪心搜索每一步只挑概率最高的那个词但这样容易陷入局部最优。Beam Search会每一步保留TopK个候选序列最后挑整体得分最高的那条。K通常取4或5代表“搜索宽度”。RoUGE指标这是摘要领域最常用的自动评测方式通过计算生成摘要和参考摘要之间的词元重合度来打分。后面会专门讲它的用法。这4个概念在网上都有大量优质讲解关键是要把它们对应到实际代码里去理解不是背概念。3. 数据准备与预处理影响模型上限的隐形环节3.1 中文摘要数据集怎么选LCSTS使用经验数据决定了模型效果的上限。我当时选择了LCSTSLarge-scale Chinese Short Text Summarization Dataset这是哈尔滨工业大学开源的一个中文短文本新闻摘要数据集特点是输入是一篇短文输出是一句话标题式的摘要。原始数据规模很大官方公开版本约240万条但通常不会全量使用很多人会按照它附带的质量评分做过滤然后取出其中质量较高的几十万条来训练。我实际跑下来的体验是用小一点的子集比如评分过滤后剩下30万到40万条训练速度可控效果也已经足够支撑毕设分析。LCSTS自带测试集省去了自己划分的麻烦。如果你想用英文数据CNN/DailyMail是最常见的选项大约有31万条新闻-摘要对处理思路类似只是分词方式不同。写代码前一定要先做统计分析搞清楚文本长度分布。比如LCSTS的正文平均长度可能在100~200字之间摘要平均在20~40字之间这个分布直接影响后面max_length的设定。不要想当然设置超参。3.2 数据清洗与长度过滤流程数据清洗是整个项目里最繁琐也最容易被低估的一步。我遇到过的情况包括正文里夹着HTML标签、特殊表情符号、新闻来源附加信息还有完全重复的文本对。这些脏数据如果不处理模型会花不少容量去拟合噪声。我的清洗流程大致是import re def clean_text(text: str) - str: # 去除HTML标签 text re.sub(r[^], , text) # 去除URL text re.sub(rhttp\S|www\.\S, , text) # 压缩连续空白字符 text re.sub(r\s, , text).strip() # 过滤掉只剩符号的无效内容 text re.sub(r^[\s。、,.!?]*$, , text) return text清洗完之后还需要做长度过滤正文太短的直接去掉比如少于20个字符的长文本没有摘要价值正文过长但超过模型最大输入长度的可以截断摘要过长的也要过滤因为过长摘要容易让训练不稳定。此外文本去重也很有必要否则重复样本会让模型偏向某几种句式。我在预处理时还做了一个小细节把正文和摘要都做了统一的标点转换全角转半角、合并重复标点。这个操作能让tokenizer处理得更稳定生成的摘要标点也会完整一些。3.3 tokenizer、词表与DataLoader构建细节一个常见的误区是自己用jieba分词构建词表然后丢给模型。如果用BERT/BART这类预训练模型千万不要自己造词表。预训练模型的权重是跟它的tokenizer严格绑定的你必须使用同一个分词器否则词表索引对不上模型会变成随机初始化。在代码里我直接使用了BartTokenizer.from_pretrained(fnlp/bart-base-chinese)。它是基于BPEByte Pair Encoding的方式对文本切分中文字符基本是单字级别的处理不需要额外分词。构建Dataset时关键是把padding、truncation和labels处理好下面这段代码可以直接参考from torch.utils.data import Dataset class SummaryDataset(Dataset): def __init__(self, texts, summaries, tokenizer, max_src_len512, max_tgt_len100): self.texts texts self.summaries summaries self.tokenizer tokenizer self.max_src_len max_src_len self.max_tgt_len max_tgt_len def __len__(self): return len(self.texts) def __getitem__(self, idx): src self.tokenizer( self.texts[idx], max_lengthself.max_src_len, truncationTrue, paddingmax_length, return_tensorspt, ) tgt self.tokenizer( self.summaries[idx], max_lengthself.max_tgt_len, truncationTrue, paddingmax_length, return_tensorspt, ) labels tgt[input_ids].clone().squeeze(0) # 将padding部分设为-100loss计算时会自动忽略 labels[labels self.tokenizer.pad_token_id] -100 return { input_ids: src[input_ids].squeeze(0), attention_mask: src[attention_mask].squeeze(0), labels: labels, }这段代码里有几个细节值得说明。paddingmax_length会让batch里每个样本维度一致省去collate_fn的额外处理但推理阶段不要这样做否则会浪费算力。labels里的pad_token_id必须替换成-100这是HuggingFace里约定俗成的做法CrossEntropyLoss会忽略label为-100的位置。还有一个容易踩的坑BART的tokenizer可能会在某些特殊token上产生不一样的索引建议代码跑起来后先打印几条编码结果检查一眼。4. 环境配置与训练脚本实操4.1 环境版本对照与安装避坑环境配置虽然不涉及算法但很多项目卡在第一步就是版本对不上。我当时使用的组合是Python 3.9PyTorch 2.0HuggingFace Transformers 4.xCUDA 11.8。这个组合在Windows 11和Linux服务器上都能比较稳定地运行。我用的是conda创建虚拟环境然后执行下面几条核心命令conda create -n textsum python3.9 -y conda activate textsum pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers datasets tqdm rouge-score这里最容易出问题的是PyTorch和CUDA的匹配。如果你的GPU是NVIDIA 30系之后的中高端卡CUDA 11.8基本都支持。安装完先做一次测试python -c import torch; print(torch.cuda.is_available(), torch.__version__)如果返回True再导入一次from transformers import BartTokenizer, BartForConditionalGeneration确保库本身没问题。我有一次在Windows上遇到torch.cuda.is_available()为False最后发现是安装的PyTorch是CPU版本重新指定CUDA版本安装就好了。如果你机器显存只有4G左右建议把max_src_len下调到256或者换用更小的模型纯CPU训练不太现实至少需要一块入门级GPU或者使用云GPU平台。4.2 微调核心代码逐段解读训练部分的核心代码并不复杂但有几个环节值得逐段看一下。首先是加载模型和设置训练参数from transformers import BartForConditionalGeneration, BartTokenizer, AdamW, get_linear_schedule_with_warmup model_name fnlp/bart-base-chinese tokenizer BartTokenizer.from_pretrained(model_name) model BartForConditionalGeneration.from_pretrained(model_name).to(device) batch_size 16 learning_rate 3e-5 epochs 3 warmup_steps 500然后是训练循环。这里有个关键点model的返回值包含了loss所以我们不需要手动计算交叉熵直接把outputs.loss拿去反传就可以。from tqdm import tqdm optimizer AdamW(model.parameters(), lrlearning_rate) total_steps len(train_loader) * epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepswarmup_steps, num_training_stepstotal_steps, ) model.train() for epoch in range(epochs): loop tqdm(train_loader, descfEpoch {epoch 1}) for batch in loop: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) outputs model( input_idsinput_ids, attention_maskattention_mask, labelslabels, ) loss outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step() optimizer.zero_grad() loop.set_postfix(lossloss.item())clip_grad_norm_这行很关键。生成模型在微调初期梯度容易暴涨梯度裁剪一般取1.0就可以能有效防止loss突然变成NaN。我最初因为没有加这行训练到第2个epoch时loss直接跳到几十整个训练崩掉浪费了不少时间。另外如果想在有限显存下增大batch size可以加上gradient_accumulation_steps比如设成2或4把梯度累加多次再更新一次参数等效于加大batch。第一个epoch训练完后一定要先在验证集上看一下效果不要盲跑完所有epoch。4.3 训练过程的监控与结果保存深度学习训练最怕“看起来跑得挺好结果全错”。我的纠错习惯是正式训练之前先用几十条小数据跑两步确认loss在下降并且生成结果不是重复乱码这之后再启动完整训练。这个小实验成本极低但能过滤掉八成以上的代码错误。训练过程中我会把每个epoch的验证集损失和ROUGE分数记录下来。简单做法是每个epoch结束后加载模型权重在验证集上做一次推理计算ROUGE。保存模型时不要只存一个model.pt最好把tokenizer一起保存model.save_pretrained(./best_model) tokenizer.save_pretrained(./best_model)这样后面加载时只需两行代码就能恢复完整的模型和分词器。我做毕设时每次都把“验证集ROUGE最高的那次”作为最终模型而不是最后一个epoch的模型这一点在答辩时也能体现你做实验的严谨性。5. 评估指标、常见问题与调优实录5.1 ROUGE指标原理与计算工具ROUGE是文本摘要最常用的自动评估指标它的核心思想很简单比较生成摘要和参考摘要的重合度。ROUGE-1看单字/词的重合ROUGE-2看连续两个词的重合ROUGE-L用最长公共子序列来计算相似度。中文语境下ROUGE指标会基于字级别或切词后的词级别分别计算。我当时用官方的rouge-score库代码非常简洁from rouge_score import rouge_scorer scorer rouge_scorer.RougeScorer([rouge1, rouge2, rougeL], use_stemmerFalse) scores scorer.score(reference_summary, generated_summary) print(scores[rouge1].fmeasure)这里有一个很重要的心理预期中文摘要的ROUGE分数不会很高。LCSTS测试集上ROUGE-1跑到0.35~0.45、ROUGE-2在0.2左右已经是相当不错的效果如果你一开始看到Rouge-1只有0.2不要慌先检查是不是生成了大量重复词或者长度差距太大。另外ROUGE只是一个近似指标它完全看不出摘要的“语义”是否正确有时候模型生成了语义正确的摘要但因为用词不同分数反而低。所以最终展示时一定要配合人工看几个案例或者做一个简单的人工评测表。5.2 训练中的典型问题与排查方法我整理了一份自己在整个项目里遇到过的典型问题列表这些问题在答辩和写论文时都很适合作为“问题分析”章节的素材。现象可能原因排查与解决办法loss不下降或直接变NaN学习率过大、梯度爆炸、数据有脏值调小学习率到1e-5加梯度裁剪检查清洗后的数据样例生成结果全是重复片段beam search没有禁用n-gram重复训练不充分推理时加no_repeat_ngram_size3适当增加训练步数显存不足OOMbatch size太大或输入过长减小batch size使用gradient accumulation或把输入截断到256验证loss正常但ROUGE低生成长度和参考摘要差异大、解码策略不合适调整生成时的max_length尝试beam size 5、length_penalty中文出现乱码或[UNK]tokenizer与权重不匹配确认使用同一预训练模型的tokenizer检查几条编码结果模型收敛特别慢训练数据量太少或学习率太低先用小样本过拟合测试确认梯度没有阻塞再加大数据量这中间最隐蔽的问题是“loss在降生成的句子却越来越短”。后来我发现是decoder侧没有约束生成长度导致模型学会了提前输出EOS。解决办法是在训练时对标签长度做过滤推理时设置min_length比如对LCSTS这种短摘要设置min_length10、max_length50效果立竿见影。5.3 调优心得与后续扩展方向做完整个项目我有一个很深的体会预训练模型的微调核心不是把模型调复杂而是把数据和评测做扎实。学习率不要贪大3e-5是比较稳妥的起点epoch数不用太多BART-base在中文摘要上微调3个epoch左右就基本收敛再多反而容易过拟合到训练集的句式。每次调整完超参一定要先在小验证集上做一次快速实验再决定是否全量训练。如果你想在这个题目上继续做扩展有几个方向可以考虑一是用mT5替换BART看看更大规模的模型能不能带来稳定提升二是在解码阶段下功夫比如用长度惩罚或关键词约束生成让摘要更可控三是给自己的流程加上错误分析把生成结果按“信息缺失”“重复冗余”“语义错误”分类统计这会让论文的工作量和技术深度都上一个台阶。最后再说一个我自己常用的技巧不要只看验证集整体指标要把生成结果按文本长度分组看。比如把测试集按原文长度分成短、中、长三组分别计算ROUGE往往能发现模型在长文本上的短板更明显。这个结论写进论文里比单纯报一个总分数更有说服力。文本摘要的难点很多时候并不在模型解读而在于数据和评估先把这两块地基打好深度学习模型的效果反而来得很快。希望这篇完整流程能帮你在开题和答辩的时候少一点焦虑。本文还有配套的精品资源点击获取
返回列表