简介基于Python的GPT-2中文摘要生成模型代码实现面向自然语言处理初学者与有一定Python基础的开发者聚焦长文本摘要场景帮助读者理解GPT-2模型加载、文本编码、摘要生成与解码的完整流程。资源包共108个文件以105个Python脚本为主另含2个文本文件与1个JSON配置涵盖模型结构定义、Tokenizer工具、生成调用及参数配置等模块整体仅458KB便于快速下载与本地调试。已有3582人学习下载适合作为入门中文文本摘要项目的参考实现。通过阅读源码可掌握transformers库与PyTorch框架的实际用法理解AutoTokenizer、AutoModelForCausalLM等关键API并可直接在自有语料上替换模型名称运行是一份紧凑实用的示例代码包。1. 基于Python的GPT2中文摘要生成模型代码实现最难的不是模型代码是样本拼接基于Python的GPT2中文摘要生成模型代码实现本质上是用自回归语言模型做条件生成把一篇中文正文喂进去让模型逐字吐出对应摘要。这类代码的公开范例不算少但很多人照着抄完、训练完发现模型输出的摘要要么是原文复读要么是一串重复的了和的问题几乎都出在样本拼接和损失计算上而不是模型本身。这个方案的意义在于用一张消费级显卡就能微调出能用的中文摘要模型很适合有Python和PyTorch基础、想在中文NLP任务上快速出结果的开发者。2. GPT-2做中文摘要的原理与选型自回归模型为什么适合短文本生成在做任何代码之前先得把为什么是GPT-2这件事想清楚。中文摘要任务市面上有不少选择抽取式方案、BART/T5微调、甚至直接用大模型API。GPT-2的优势不在于它最强而在于它在单卡可训练、中文生成流利、代码生态成熟这三个约束下是最稳的起点。2.1 摘要任务如何变成下一个词预测GPT-2是一个decoder-only的自回归模型结构上只有带因果掩码的Transformer解码器。所谓因果掩码就是计算第i个位置的注意力时只能看到前i-1个位置。这个结构天生适合做生成任务模型每生成一个词就把这个词拼到输入末尾继续预测下一个词。摘要任务可以被建模成给定正文预测摘要序列的条件语言模型。数学上可以理解为模型把P(摘要|正文)这个概率分解成每一步的条件概率相乘每一步都基于正文和已经生成的摘要词来预测下一个词。训练时我们把正文和摘要拼成一个序列让模型在这个序列上做标准的语言模型训练推理时只给正文让模型自己往后面生成。这里有一个关键操作训练loss不能在整个拼接序列上计算而必须只在摘要部分计算。如果不做这个区分模型学到的策略就变成把正文原样复述一遍最安全因为复述正文的loss非常低。后面避坑章节会专门讲这个。2.2 中文摘要选GPT-2而不是BART/T5的四个现实理由第一是显存和参数量。GPT-2基础版参数量在1亿出头BART和T5的base版虽然参数相近但它们是encoder-decoder双塔结构前向计算要走两遍激活值占用明显更高。同样在12G显存下GPT-2可以开到batch size 8配合梯度累积BART可能只能开到4。对中文摘要这种通常只有几万到几十万条数据的任务来说模型容量太大反而更容易过拟合。第二是中文预训练权重的可获得性。社区里有不少中文GPT-2预训练权重训练语料涵盖新闻、百科、社交媒体直接把model_name换成权重目录就能加载不需要自己从零预训练。这一点在实践里非常关键因为摘要模型是否好用很大程度取决于预训练阶段见过的中文文本量。第三是生成流利度。自回归模型在预训练阶段做的就是纯文本语言建模预测下一个词这个目标本身逼迫模型学习中文的句法和搭配习惯。在短文本摘要场景下同等规模下GPT-2生成的中文往往比BART/T5更顺滑。这不是绝对结论但在我实际对比过的几次微调结果里这个趋势是存在的。第四是推理部署更简单。生成时只有一个decoder显存占用低甚至CPU推理也能接受。如果后面要把模型接到服务里单塔模型在工程上少很多麻烦。2.3 这个方案的三条能力边界先说输入长度。GPT-2的位置编码通常限制在512或1024个token中文一个字大约对应1到1.5个token所以正文超过几百字就得截断。如果你的语料是长篇报告摘要依赖全文信息这个方案会明显吃力。再说压缩比。新闻标题式摘要几百字正文压成一句话是GPT-2的舒适区但如果正文6000字要求压成20字标题模型输出容易变得空洞、套话化。压缩比超过10比1时建议换更大的模型或改用抽取生成的混合方案。最后是事实一致性。生成式模型天然有幻觉问题摘要里可能出现原文没有的信息。做新闻摘要、医疗摘要这类对准确性要求高的场景必须加人工抽检或事后规则校验不能完全信任模型输出。3. 环境搭建与加载中文GPT-2权重从空目录到跑通第一段生成原理讲清楚了下面进入能复现的部分。这一章的目标是在一台干净的机器上从零装好依赖加载中文GPT-2权重并确认模型真的能用。整个过程不需要GPU也能完成加载和测试但训练必须要GPU。3.1 依赖安装与版本取舍先创建虚拟环境避免把系统Python环境搞乱。这里我用venv而不是conda因为它在纯Python环境下开箱即用。如果是Windows把source venv/bin/activate换成venv\Scripts\activate。# 创建并激活虚拟环境 python3 -m venv venv source venv/bin/activate # 升级pip并安装核心依赖 pip install --upgrade pip pip install torch pip install transformers datasets rouge_score逻辑说明torch是训练后端transformers负责加载GPT-2模型和分词器datasets用来加速数据读取rouge_score用于摘要质量评估。这里没有锁版本号但建议用PyTorch 2.x搭配transformers 4.x系列这是当前最稳的组合。参数说明如果你的机器有NVIDIA GPU先确认CUDA版本再去PyTorch官网生成对应的安装命令不要直接装CPU版。显存低于6G的话训练会非常紧建议配合后面的梯度累积和gradient checkpointing技巧低于4G基本只能做推理训练会很艰难。3.2 加载中文GPT-2权重的最小代码依赖装好后用下面的代码验证加载链路。这里的model_name是关键你需要替换成实际的中文GPT-2权重目录。from transformers import AutoTokenizer, AutoModelForCausalLM # 换成你本地存放的中文GPT-2权重目录 model_name ./chinese-gpt2 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained(model_name) # 检查分词器是否正常工作 sample 中文摘要生成模型测试 ids tokenizer.encode(sample) assert len(ids) 20, f分词结果异常: {ids} print(f词表大小: {tokenizer.vocab_size}) print(f模型参数量: {sum(p.numel() for p in model.parameters()) / 1e6:.1f}M)逻辑说明AutoTokenizer会根据权重目录里的tokenizer配置自动加载分词器负责把中文句子拆成子词AutoModelForCausalLM则根据权重目录里的config文件自动构建模型结构。最后两行打印用于确认加载正确中文权重词表大多在2万到5万之间参数量在100M左右如果数字明显偏离说明权重目录选错了。参数说明model_name既可以是本地目录也可以是社区公开的权重标识。但不建议直接依赖某个在线标识稳妥做法是下载到本地再用本地路径加载避免训练时网络波动中断。3.3 加载成功后必做的三个自查第一个自查是生成一句中文看看通不通顺。写一个最简推理代码输入今天天气不错我们看模型能不能续写出合理的中文。如果输出乱码或英文大概率是tokenizer和模型不匹配。第二个自查是查看特殊token。很多中文GPT-2权重只定义了eos_tokenpad_token是空的。打印tokenizer.pad_token和tokenizer.eos_token如果pad是None后面做batch训练时padding会出问题需要在训练前手动补上。第三个自查是确认分词粒度。试几个摘要里的高频词比如根据会议表示看看被拆成几个token。如果每个词都被拆成七八个碎片说明词表覆盖不足模型生成时容易产生不通顺的拼凑词。4. 数据处理与训练代码把中文语料拼成可学习的摘要样本这一章是整个方案的核心。数据处理的质量直接决定模型能不能收敛出像样的摘要。很多项目在模型选型和训练参数上花了大量精力最后发现效果差是数据格式错了这是最亏的。4.1 训练样本的格式设计与清洗规则GPT-2没有encoder所有条件信息只能以文本前缀的形式放在序列前面。所以训练样本的格式是固定的正文 [SEP] 摘要 [EOS]。[SEP]告诉模型正文到这里结束下面开始是摘要[EOS]告诉模型摘要生成完毕。数据文件推荐用JSONL格式每行一个样本字段只有两个简单直接{article: 这是一段待摘要的正文内容模型需要从这段文字中提取核心信息并生成一句话摘要。, summary: 一段正文内容的摘要示例}清洗规则我一般按四步走。第一步统一转成UTF-8编码并去除HTML标签数据里偶尔混着网页标签会直接污染生成结果。第二步过滤过短或过长的样本正文少于50个字的大多信息量不足正文超过450个字的必须截断因为加完分隔符和摘要后长度会逼近512上限。第三步过滤乱码样本文本里出现连续替换字符的整条丢弃。第四步简体繁体统一这一步会让生成结果稳定很多常用做法是接入社区开源的简繁转换库。4.2 构造Dataset与collate_fn数据清洗完接下来是核心的Dataset封装。这里最值得注意的就是labels的构造——正文部分的标签必须是-100这样计算loss时PyTorch会自动跳过这些位置。import json import torch from torch.utils.data import Dataset class SummaryDataset(Dataset): def __init__(self, data_path, tokenizer, max_length512): self.samples [] with open(data_path, r, encodingutf-8) as f: for line in f: self.samples.append(json.loads(line)) self.tokenizer tokenizer self.max_length max_length def __len__(self): return len(self.samples) def __getitem__(self, idx): item self.samples[idx] article_ids self.tokenizer.encode(item[article], add_special_tokensFalse) summary_ids self.tokenizer.encode(item[summary], add_special_tokensFalse) # 正文最多占序列的7/8给摘要预留空间 max_article_len int(self.max_length * 0.875) - 2 article_ids article_ids[:max_article_len] summary_ids summary_ids[: self.max_length - len(article_ids) - 3] input_ids ( [self.tokenizer.cls_token_id] article_ids [self.tokenizer.sep_token_id] summary_ids [self.tokenizer.eos_token_id] ) # 关键正文部分标签设为-100不参与损失计算 labels [-100] * (2 len(article_ids)) summary_ids [self.tokenizer.eos_token_id] return {input_ids: input_ids, labels: labels}逻辑说明cls和sep的token_id是固定的两个位置所以正文部分的mask数量是2 len(article_ids)。labels的构造是整个训练能不能学好的决定性细节——模型只会在摘要token上计算loss不会学着复读正文。collate_fn负责把一个batch的样本动态padding到相同长度PAD位置在labels里对应-100避免模型学到生成PADdef collate_fn(batch, tokenizer): input_ids [torch.tensor(x[input_ids], dtypetorch.long) for x in batch] labels [torch.tensor(x[labels], dtypetorch.long) for x in batch] input_ids torch.nn.utils.rnn.pad_sequence( input_ids, batch_firstTrue, padding_valuetokenizer.pad_token_id ) labels torch.nn.utils.rnn.pad_sequence( labels, batch_firstTrue, padding_value-100 ) attention_mask (input_ids ! tokenizer.pad_token_id).long() return {input_ids: input_ids, attention_mask: attention_mask, labels: labels}参数说明动态padding比固定padding省显存。如果整个数据集都pad到512短样本多的场景下显存浪费非常可观。4.3 训练循环与关键超参数用Trainer还是手动训练循环我的建议是第一次跑通用Trainer做实验要精细控制梯度累积时用手动循环。这里给出手动循环版本它能清楚看到每个关键点的作用。from torch.utils.data import DataLoader from transformers import get_linear_schedule_with_warmup # tokenizer需要先补pad_token并同步resize模型词嵌入 if tokenizer.pad_token is None: tokenizer.add_special_tokens({pad_token: [PAD]}) model.resize_token_embeddings(len(tokenizer)) dataset SummaryDataset(train.jsonl, tokenizer) train_loader DataLoader( dataset, batch_size4, shuffleTrue, collate_fnlambda b: collate_fn(b, tokenizer), ) optimizer torch.optim.AdamW(model.parameters(), lr3e-5) epochs 3 gradient_accumulation_steps 4 total_steps len(train_loader) * epochs // gradient_accumulation_steps scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_steps200, num_training_stepstotal_steps ) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) model.train() for epoch in range(epochs): for step, batch in enumerate(train_loader): 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 / gradient_accumulation_steps loss.backward() if (step 1) % gradient_accumulation_steps 0: optimizer.step() scheduler.step() optimizer.zero_grad() if step % 500 0: print(fepoch {epoch} step {step} loss {loss.item() * gradient_accumulation_steps:.4f})逻辑说明这里的核心是labelslabelsTrainer和手动循环都一样模型内部会自动忽略-100位置。梯度累积的意思是把4个batch的梯度攒起来更新一次等效于batch size 16用于弥补显存不足。关键超参数经验值如下参数推荐值说明learning_rate2e-5 ~ 5e-5微调阶段用3e-5比较稳数据量大建议往2e-5靠batch_size4 ~ 8显存小就靠梯度累积凑等效batchmax_length512中文权重通常支持的最大长度epochs2 ~ 4数据20万条以上时2轮足够小数据可以到4轮warmup前10%步数防止训练初期loss震荡过大gradient_accumulation_steps2 ~ 8在等效batch 16到32之间调训练过程中需要盯两个信号一是loss是否平滑下降二是每隔几百步做一次小验证。这里的小验证不是算ROUGE而是拿几条没见过的正文让模型生成摘要肉眼看有没有学会先读正文再输出这个基本模式。5. GPT-2中文摘要训练避坑五个高频翻车现场与修复方法训练GPT-2做中文摘要模型代码本身不出错出错的都是数据、tokenizer和生成参数这些看起来不起眼的地方。下面五个坑是我认为出现频率最高的每一条都按现象、原因、解决三步说清楚。5.1 训练loss正常下降生成结果却疯狂重复现象训练loss从5.x稳步降到2.x看起来一切正常结果生成的摘要出现根据根据根据或整段重复同一句话。原因最常见的翻车现场。训练时没有把正文部分的labels设为-100模型发现复读原文是降低loss最轻松的策略于是学会了抄写而不是概括。loss再低产出也是废的。解决回到4.2节的Dataset实现检查labels构造逻辑。正文部分必须是-100只有摘要和[EOS]位置参与loss计算。推理时再加一道保险no_repeat_ngram_size3禁止3元组重复。5.2 生成结果只输出一个[SEP]就没了现象输入正文模型生成到[SEP]位置后立刻输出[EOS]摘要为空或只有几个字。原因训练数据的摘要部分没有以[EOS]结尾或者tokenizer的eos_token_id与模型config里的不一致。模型学到的是[SEP]后面就结束自然不愿意继续生成。解决检查数据拼接代码确认每个样本都以tokenizer.eos_token_id结尾再打印tokenizer.eos_token_id和model.config.eos_token_id如果不一致以tokenizer为准强制对齐。5.3 pad_token是None引发的连锁翻车现象训练时控制台刷padding警告batch推理时输出一串pad pad pad模型保存后再加载生成结果变成乱码。原因很多中文GPT-2预训练权重只定义了eos_tokenpad_token未设置。transformers默认用token_id 0做padding但0在词表里是有效字符。模型训练时见过PAD位置参与计算推理时就会把PAD当正常文本输出。解决加载模型后立即补上pad_token注意必须同步resize词嵌入这两步缺一不可if tokenizer.pad_token is None: tokenizer.add_special_tokens({pad_token: [PAD]}) model.resize_token_embeddings(len(tokenizer))提示resize_token_embeddings这步漏掉会直接报维度不匹配的错误所以看到报错先检查这里。5.4 单卡训练显存OOMbatch size降到1还是炸现象batch_size设为1max_length设为512依然CUDA OOM。原因GPT-2的注意力复杂度是序列长度的平方512长度的激活值占用远大于短序列。但更隐蔽的原因是padding策略——如果整个数据集都固定pad到512短样本占比高时显存被无谓浪费。解决先用4.2节的动态padding还不行就开启gradient checkpointing用少量计算换显存model.gradient_checkpointing_enable()这个开关配合batch_size 2和梯度累积能把可训练的最大序列长度从512推到1024左右。代价是训练速度会慢20%到30%但总比OOM强。5.5 验证集ROUGE分数很高人工抽看却很差现象ROUGE-1/2/L都到0.4以上信心满满去抽看生成结果发现摘要就是把原文第一句话截断或简单改写。原因中文新闻类摘要数据集里很大比例的训练样本本身就是原文首句的压缩改写。模型学到的是抄前几句这个捷径ROUGE对词汇重叠敏感这种抄袭式摘要得分反而高。解决在验证集里加入必须推理才能概括的样本单独计算这部分ROUGE同时加一个多样性指标比如统计生成结果的distinct n-gram比例n-gram重复率过高说明模型在绕弯子。人工抽10条看一遍永远是最后一道防线。6. 推理参数调优与质量验证让生成摘要真正可用模型训练完最后一步是推理参数的选择。这一步最容易被忽略但同样的模型生成参数不同效果能差出一大截。6.1 用ROUGE加distinct双重评估评估代码可以直接用rouge_score库中文摘要推荐用rougeLsum作为主指标它对换行和标点更稳健from rouge_score import rouge_scorer scorer rouge_scorer.RougeScorer([rouge1, rouge2, rougeLsum], use_stemmerFalse) for ref, hyp in zip(reference_summaries, generated_summaries): scores scorer.score(ref, hyp) print(fROUGE-L: {scores[rougeLsum].fmeasure:.4f})同时统计生成结果的distinct 2-gram比例低于0.3说明句子重复严重需要调高no_repeat_ngram_size或降低temperature。6.2 推理参数推荐组合摘要生成建议用带温度采样的随机解码而不是纯beam search。beam search在中文摘要任务里容易输出最大公约数式的平淡文本采样在多样性和准确性之间更容易平衡。参数推荐值说明max_new_tokens80 ~ 120按目标摘要长度调整do_sampleTrue开启采样式生成top_p0.85保留累计概率85%的候选词top_k50先截断到概率最高的50个词temperature0.8中文摘要0.7到0.9之间调no_repeat_ngram_size3禁止3元组重复防复读推理代码model.eval() with torch.no_grad(): article_ids tokenizer.encode(article, add_special_tokensFalse, max_length480, truncationTrue) input_ids torch.tensor([[tokenizer.cls_token_id] article_ids [tokenizer.sep_token_id]]).to(device) output_ids model.generate( input_ids, max_new_tokens80, do_sampleTrue, top_p0.85, top_k50, temperature0.8, no_repeat_ngram_size3, eos_token_idtokenizer.eos_token_id, pad_token_idtokenizer.pad_token_id, ) summary tokenizer.decode(output_ids[0][input_ids.shape[1]:], skip_special_tokensTrue)切片取input_ids.shape[1:]是把输入前缀去掉只保留新生成的摘要部分。我最早跑这个方案时觉得训练完就万事大吉结果连续几天都耗在和ROUGE分数较劲上。后来养成一个习惯每次微调完先抽十条生成结果人工看一遍再决定要不要调参数这个习惯省下来的时间远比花在调参上的多。希望帮到你。本文还有配套的精品资源点击获取