news 2026/10/18 11:58:05

GPT-2中文摘要生成模型Python代码实现与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GPT-2中文摘要生成模型Python代码实现与避坑指南

简介:基于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_token,pad_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_length=512): self.samples = [] with open(data_path, "r", encoding="utf-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_tokens=False) summary_ids = self.tokenizer.encode(item["summary"], add_special_tokens=False) # 正文最多占序列的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,避免模型学到生成PAD:

def collate_fn(batch, tokenizer): input_ids = [torch.tensor(x["input_ids"], dtype=torch.long) for x in batch] labels = [torch.tensor(x["labels"], dtype=torch.long) for x in batch] input_ids = torch.nn.utils.rnn.pad_sequence( input_ids, batch_first=True, padding_value=tokenizer.pad_token_id ) labels = torch.nn.utils.rnn.pad_sequence( labels, batch_first=True, 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_size=4, shuffle=True, collate_fn=lambda b: collate_fn(b, tokenizer), ) optimizer = torch.optim.AdamW(model.parameters(), lr=3e-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_steps=200, num_training_steps=total_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_ids=input_ids, attention_mask=attention_mask, labels=labels, ) 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(f"epoch {epoch} step {step} loss {loss.item() * gradient_accumulation_steps:.4f}")

逻辑说明:这里的核心是labels=labels,Trainer和手动循环都一样,模型内部会自动忽略-100位置。梯度累积的意思是把4个batch的梯度攒起来更新一次,等效于batch size 16,用于弥补显存不足。

关键超参数经验值如下:

参数推荐值说明
learning_rate2e-5 ~ 5e-5微调阶段用3e-5比较稳,数据量大建议往2e-5靠
batch_size4 ~ 8显存小就靠梯度累积凑等效batch
max_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_size=3,禁止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_token,pad_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 单卡训练显存OOM,batch size降到1还是炸

现象:batch_size设为1,max_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_stemmer=False) for ref, hyp in zip(reference_summaries, generated_summaries): scores = scorer.score(ref, hyp) print(f"ROUGE-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_tokens=False, max_length=480, truncation=True) input_ids = torch.tensor([[tokenizer.cls_token_id] + article_ids + [tokenizer.sep_token_id]]).to(device) output_ids = model.generate( input_ids, max_new_tokens=80, do_sample=True, top_p=0.85, top_k=50, temperature=0.8, no_repeat_ngram_size=3, eos_token_id=tokenizer.eos_token_id, pad_token_id=tokenizer.pad_token_id, ) summary = tokenizer.decode(output_ids[0][input_ids.shape[1]:], skip_special_tokens=True)

切片取input_ids.shape[1:]是把输入前缀去掉,只保留新生成的摘要部分。我最早跑这个方案时,觉得训练完就万事大吉,结果连续几天都耗在和ROUGE分数较劲上。后来养成一个习惯:每次微调完先抽十条生成结果人工看一遍,再决定要不要调参数,这个习惯省下来的时间远比花在调参上的多。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 11:57:46

Go后端利器:struct-to-graphql自动生成GraphQL Mutation实战

干后端这些年&#xff0c;GraphQL项目里最让我上火的不是查询解析&#xff0c;而是Mutation。Query好歹能靠嵌套查询偷点懒&#xff0c;Mutation是彻底没办法&#xff1a;输入参数、返回类型、解析函数、数据库写入&#xff0c;一整条链路全得手工串起来&#xff0c;字段一多&a…

作者头像 李华
网站建设 2026/10/18 11:57:07

FFmpeg libx265 高清视频编解码实战:参数调优与避坑指南

简介&#xff1a;这是一份面向多媒体开发者的FFmpeg HEVC编解码实战示例&#xff0c;聚焦H.265这一高效节省带宽的高清视频编码标准&#xff0c;帮助读者理解解复用、解码、编码、Packet处理与输出写入的完整链路。资源包共1个文件&#xff0c;为单个C源码文件&#xff0c;压缩…

作者头像 李华
网站建设 2026/10/18 11:53:30

Makefile实战指南:从增量构建到自动依赖管理

1. Makefile到底解决了什么问题——从手动编译到增量构建我最早接触Makefile的时候&#xff0c;说实话挺抗拒的。当时手里项目不大&#xff0c;也就十几个.c文件&#xff0c;每次编译直接在终端敲一条长长的gcc命令&#xff0c;把源文件一个个拼上去&#xff0c;从痛苦到麻木也…

作者头像 李华
网站建设 2026/10/18 11:52:04

AI辅助文献综述:从检索到定稿的10款工具与实操流程

本科写文献综述这事&#xff0c;我太有发言权了。当年我为了凑一篇课程综述&#xff0c;硬是在图书馆从下午坐到凌晨两三点&#xff0c;眼睛都快瞎了&#xff0c;电脑里开着二十来个PDF标签页&#xff0c;翻来翻去就是"看了忘、忘了看"&#xff0c;最后憋出来的综述还…

作者头像 李华