简介:基于Python的GPT-2中文摘要生成模型代码实现,面向自然语言处理初学者、学生以及需要快速搭建中文摘要系统的开发者。资源完整覆盖了从Hugging Face加载预训练中文GPT-2模型、使用AutoTokenizer对长文本分句编码、调用generate方法生成摘要,到用tokenizer.decode还原可读文本的完整流程;代码中给出了max_length、num_beams、early_stopping等关键参数的设置方式,并解释了如何通过Finetuning适配特定领域文本,以及BERTSUM、Lead-3等替代方案对比等优化思路。压缩包共108个文件,以105个Python源码文件为主,涵盖模型定义、分词器封装、推理脚本和调用示例,另有2个txt说明文档与1个json模型配置文件,整体仅458KB,目录结构清晰,便于按模块阅读和二次修改。目前已有3580人学习下载,适合边看边练,快速掌握GPT-2在中文摘要生成任务中的实际落地路径。
1. 用 GPT-2 做中文摘要生成:这个 Python 代码包到底能解决什么问题
做中文摘要生成的时候,很多人的第一反应是去调现成的文本摘要 API,但如果你面对的是新闻聚合、周报自动生成、长工单内容浓缩这类场景,API 的返回长度、语气和风格都没法按自己的需求控制。这时候用 Python 在本地跑一个 GPT-2 中文摘要生成模型反而更踏实:模型权重在自己手里,输入输出都走本地代码,生成多少字、带不带重复惩罚都由你说了算。GPT-2 虽然是 OpenAI 发布的自回归语言模型,但它的中文变体在文本续写和压缩理解上表现稳定,是入门生成式摘要最合适的模型之一。
这份资源不只是一段示例脚本,而是一个完整的代码包。解压后你能看到 model_config_dialogue_small.json 这样的模型配置文件,也有 modeling_bert.py、modeling_xlnet.py、modeling_auto.py、tokenization_utils.py 等一批 transformers 内核源码文件。换句话说,你拿到的不只是"怎么调用现成模型",还包括"如果要改模型配置、看内部结构该从哪里下手"。适合想做中文摘要但不想把整个流程当成黑匣子的人,也适合需要把摘要模型集成进现有 Python 服务里的工程师。
2. 自回归模型与中文摘要:为什么 GPT-2 适合但又不完全适合
2.1 GPT-2 的生成原理:一个 token 一个 token 地续写
GPT-2 的核心是一个 decoder-only 的 Transformer 结构,训练目标是标准的自回归语言建模:给定前面所有 token,预测下一个 token 的概率分布。生成的时候也是一个 token 一个 token 地往右续写,每一轮都把已经生成的内容拼回输入,再预测下一个词。这种机制决定了它天生适合"给一段话,让它把剩余部分写出来"的任务,而摘要本质上就是一种受限的续写——模型读完整篇文章后,以开头标记为起点写出一个更短的版本。
代码包里的 modeling_utils.py 承担了权重初始化、注意力掩码这些底层公共逻辑,modeling_auto.py 则根据配置文件自动决定实例化哪个模型类。做中文摘要时你大概率不会直接改这些文件,但理解了它们的分工,后面排查加载问题时就能少走弯路。比如 generate 时如果报 attention mask 相关的错误,问题往往出在注意力掩码的构造逻辑,而不是模型本身。
GPT-2 的生成质量高度依赖解码策略。贪心解码每次都取概率最大的 token,速度快但容易陷入重复;beam search 保留多个候选路径,能让整体概率更高,但候选宽度太大会让文本变得平淡。后面第 4 章会对这些参数做具体说明,这里先建立概念:摘要生成是概率搜索问题,不完全是"模型能力"问题。
2.2 中文摘要的三重难点:分词、指代和主题漂移
中文摘要比英文摘要难,首先是分词。英文天然按空格切词,中文没有显式边界,同样的"研究"既可以当动词也可以当名词。中文 GPT-2 的 tokenizer 通常混合了字级和词级切分,遇到人名、机构名、生僻字时很容易把一个完整语义切开。tokenization_utils.py 里封装了这些编码规则,如果你发现生成的摘要里专有名词被拦腰截断,需要先怀疑 tokenizer 的词表覆盖,而不是模型的生成能力。
第二个难点是指代。长文章里大量出现"该公司""这个方案""上述方法"这类指代词,GPT-2 在逐 token 生成时,如果窗口长度不够或者前文信息被注意力机制稀释,指代关系就容易丢,摘要里就会出现"该方案的效果很好"但读者根本不知道方案是什么。我一般会在生成前把文章的关键实体、时间、数字抽取出来拼到 prompt 前面,用显式上下文减轻指代负担。
第三个难点是主题漂移。摘要生成的结果应当围绕原文主题展开,但自回归模型在长尾生成时倾向于顺着自己最近生成的几个词往下写,写到后面就跑偏了。这时候光靠模型自身不够,no_repeat_ngram_size 这类参数只能治重复,治不了主题漂移。更靠谱的办法是把长文章分句后再逐句生成,每句的 prompt 都重新对齐主题,这在第 4 章的完整流程里会展开。
2.3 代码包文件清单:modeling_*.py 和 tokenization_utils.py 各管什么
解压这份资源后,先不要急着跑,把文件认一遍。项目正文里列出的核心文件和我平时维护的 transformers 子集基本一一对应:
| 文件名 | 作用 |
|---|---|
| model_config_dialogue_small.json | 小规模对话模型的超参数配置,包含层数、注意力头数、词表大小等 |
| modeling_utils.py | 模型通用工具,权重初始化、梯度检查点、注意力掩码的底层实现 |
| modeling_auto.py | AutoTokenizer / AutoModel 的自动分发入口,按配置选择模型类 |
| modeling_bert.py | BERT 骨干的实现代码 |
| modeling_xlnet.py | XLNet 骨干的实现代码 |
| modeling_xlm.py | XLM 骨干的实现代码 |
| modeling_tf_bert.py / modeling_tf_xlnet.py / modeling_tf_auto.py | TensorFlow 版对应实现 |
| tokenization_utils.py | 分词、编码、解码的基础工具,所有 tokenizer 都依赖它 |
这里有一个容易被误解的地方:这份代码包里没有单独列出 modeling_gpt2.py,并不代表它不支持 GPT-2。modeling_auto.py 在加载时会把配置里的 model_type 映射到对应类,真正运行的 GPT-2 建模代码在 transformers 的 modeling_gpt2.py 里,这份资源把与 GPT-2 相关的前置依赖文件单独抽了出来。你实际跑通时需要依赖环境里安装的 transformers 版本提供 modeling_gpt2 文件,或者从代码包对应的 transformers 版本里一并拷贝进来。第一次跑如果看到"GPT2LMHeadModel is not registered in the AutoModel"之类的报错,基本就是这个映射关系没对上。
model_config_dialogue_small.json 是另一个值得先读的文件。它定义的是一个小规模的"对话模型"配置,层数比完整 GPT-2 小得多。在资源有限的本地机器上,你可以拿它作为微调的起点,而不是直接加载 1.5 亿参数的大模型。用 json 工具打开看一眼,把 n_layer、n_head、n_embd 这几个值和你的显存对应起来,后面调参就有据可依了。
3. 环境搭建与模型加载:把代码包变成能跑的模型
3.1 Python 环境与依赖版本:torch 和 transformers 怎么选
我拿到这个代码包的第一件事是新建一个干净的虚拟环境,不往系统 Python 里装。原因很简单:transformers 的版本迭代很快,modeling_auto.py 这种自动分发文件在不同版本里的类注册方式不一样,如果之前装过别的版本,很容易出现"代码包里的 modeling_auto.py 和我装的 transformers 不匹配"的怪问题。
python -m venv venv source venv/bin/activate pip install --upgrade pip pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install "transformers>=4.30,<5" pip install datasets rouge_score这段命令的逻辑是先建虚拟环境再装依赖。torch 的 --index-url 参数指定了 CUDA 11.8 的 wheel 源,如果你没有 NVIDIA 显卡,去掉这个参数直接 pip install torch 就会安装 CPU 版本。transformers 我习惯指定一个大版本范围,避免装到刚发布还没踩过坑的新版。datasets 和 rouge_score 是后面做微调和评估要用的,现在装上省得再建一次环境。
参数上有一个容易忽略的地方:CUDA 版本要和显卡驱动匹配。你可以先跑 python -c "import torch;print(torch.version,torch.cuda.is_available())" 确认一下,输出里 cuda.is_available 为 False 就说明 torch 白装了,需要回退到 CPU 版本或者换 cu118/cu121 的源。
提示:虚拟环境名不要起得太随意,我见过有人起名 venv 然后在多个项目目录里反复进错环境的,调试了半天才发现模型一直跑在别人的依赖里。
3.2 加载模型:AutoModelForCausalLM 与本地代码包的配合
模型加载是整个流程里最容易被"黑匣子化"的一步。很多人觉得 from_pretrained 一句话就完事,但其实这一句背后做了配置读取、权重文件映射、模型类分发三件事。用代码包里的 modeling_auto.py 配合 transformers 的 AutoModelForCausalLM,加载一个中文 GPT-2 模型通常是这样写的:
from transformers import AutoTokenizer, AutoModelForCausalLM model_dir = "./gpt2_chinese" # 本地模型目录,需包含 config.json 和 pytorch_model.bin tokenizer = AutoTokenizer.from_pretrained(model_dir) model = AutoModelForCausalLM.from_pretrained(model_dir) model.eval()这里 model_dir 指向本地目录,比直接传 Hugging Face 仓库名更可控,尤其是在内网服务器上部署时,权重文件下载到本地再加载是最稳的做法。常见做法是先从一个中文 GPT-2 预训练仓库把权重下载到本地,比如 uer/gpt2-chinese-cluecorpussmall 这类在中文语料上训练过的模型,然后修改 model_dir 路径指向你保存权重的目录。model.eval() 这一步看起来简单,但忘掉的话,Dropout 层还会在工作,生成的随机性会比预期大。
如果你要用代码包里 model_config_dialogue_small.json 这个配置来初始化一个小模型而不是加载预训练权重,可以把 json 配置转成 GPT2Config 后传入 from_pretrained:
from transformers import GPT2Config, GPT2LMHeadModel config = GPT2Config.from_json_file("./model_config_dialogue_small.json") model = GPT2LMHeadModel(config=config)这样加载出来的是一个随机初始化的小模型,不能直接用来生成摘要,但它是后面微调的起点。参数上要注意 GPT2Config 的字段名和 json 里的 key 必须一致,比如 n_layer 和 num_hidden_layers 是同一回事在不同版本里的两种叫法,对不上就先用 model.config.to_dict() 打印出来对比。
3.3 文本编码:encode_plus 处理中文长文本
GPT-2 的输入必须是 token id 序列,不能直接喂字符串。tokenizer 的 encode_plus 把"分词、映射 id、截断、padding"一次性做完。中文文本的处理我一般封装成一个函数:
def encode_text(text, tokenizer, max_len=512): inputs = tokenizer.encode_plus( text, return_tensors="pt", # 返回 PyTorch 张量 max_length=max_len, # 超过 512 的部分截断 padding="max_length", # 不足 512 的部分补 padding token truncation=True ) return inputs这段代码里最关键的是 padding="max_length"。它会把所有样本都补齐到 max_len,好处是批量推理时张量形状一致,坏处是 padding 位置也被 model 看到了。GPT-2 的注意力掩码本来就应该把 padding 位置遮蔽掉,所以后面调 generate 时一定要把 inputs["attention_mask"] 一起传进去,否则模型会把 padding token 也当成正常文本去续写,生成结果里会混进一堆重复的填充符。
max_length 参数对中文尤其敏感。512 个 token 在英文里对应大约 400 个词,但在中文里对应 512 个字左右,信息密度高出一截。如果你处理的文章超过 2000 字,简单截断会丢掉关键信息,这也是第 4 章要做分句处理的原因。批量场景下我习惯直接用 tokenizer.batch_encode_plus,传入一个句子列表,配合 padding=True 自动对齐最长的样本,比手动指定 max_length 省一点 token 预算。
4. 生成摘要的完整流程:从分句到解码的参数调优
4.1 分句与逐句摘要:控制输入长度
长文本直接塞给 GPT-2 有两个问题:一是超过 max_length 会被截断,文章后半段的关键信息直接消失;二是注意力机制在超长输入上会出现远程衰减,模型对前面的内容记忆不牢。我一般不会让模型一次性读完整篇文章,而是先按句末标点分句,每句单独生成一个小摘要,最后合并。
import re def split_sentences(text): parts = re.split(r'(?<=[。!?!?])', text) return [p.strip() for p in parts if p.strip()] text = "这里是需要摘要的长文本。第一句包含背景信息。第二句提出核心方案。" sentences = split_sentences(text) print(sentences)这个正则里的 (?<=[。!?!?]) 是零宽断言,表示只在标点符号后面切分,不消耗标点字符本身。这样分句后每个句子的末尾还带着句号,模型续写时语义更自然。分句后如果单句仍然超过 512 token,就对句子再做一次截断,但这种情况在中文文本里很少见。
分句策略的度要把握好。按句号切分是最粗的粒度,如果一篇文章每句话都很长、信息密度很高,可以再按逗号拆成更小的片段。反之,如果分得太碎,每个片段缺少上下文,生成的摘要句之间会缺乏衔接。我习惯先用句号切,看一下平均长度,超过 100 字再考虑二级切分。切分后记得过滤掉空字符串和只包含空白符的碎片,re.split 在句尾连续出现标点时会留下空片段,直接送进模型会导致编码异常。
提示:分句前先统计一下原文字数,如果全文不到 200 字,没有必要分句,直接整篇送进去反而能让模型看到完整的逻辑链条。
4.2 generate 参数调优:max_length、num_beams、no_repeat_ngram_size
生成摘要的质量,一半靠模型,一半靠 generate 的参数。同一个模型,参数不同,输出差异巨大。下面是一组我在中文摘要场景里比较顺手的配置:
summary_ids = model.generate( inputs["input_ids"], attention_mask=inputs["attention_mask"], # 遮蔽 padding 位置 max_new_tokens=128, # 生成的摘要长度 num_beams=4, # beam search 宽度 no_repeat_ngram_size=3, # 3-gram 重复惩罚 early_stopping=True, temperature=0.8, # 采样温度 top_p=0.9 # 核采样阈值 )| 参数 | 作用 | 建议值 | 踩坑点 |
|---|---|---|---|
| max_new_tokens | 新生成文本的最大 token 数 | 64~128 | 不是总长度,太长容易跑偏 |
| num_beams | beam search 候选路径数 | 1~4 | 大于 1 时 temperature 不生效 |
| no_repeat_ngram_size | 禁止 n-gram 重复 | 3 | 设太大会让摘要句式过于生硬 |
| early_stopping | beam 停止条件 | True | 设 False 会生成到 max 才停 |
| temperature | 采样温度 | 0.7~0.9 | 低于 0.5 接近贪心,文本干巴 |
| top_p | 核采样累积概率阈值 | 0.9 | 与 temperature 同时使用效果更稳 |
max_new_tokens 是相对新生成的长度而言的,指定生成摘要最多 128 个 token,而不是整个输出序列 128。这个值按摘要需求调,两到三句话的摘要 64 到 128 都合理。num_beams=4 表示保留 4 个候选路径,beam 越大整体越平滑,但计算量线性增长,而且太大时会生成千篇一律的句式。no_repeat_ngram_size=3 禁止同一个 3-gram 重复出现两次以上,是治"复读机"最直接的参数。
temperature 和 top_p 只在采样模式生效。temperature 越低越保守,0.8 是一个平衡点,低于 0.5 会接近贪心,摘要会变得干巴;top_p=0.9 限制只在累积概率 0.9 的 token 里采样。需要说明的是,num_beams 大于 1 时 generate 默认走 beam search,temperature 和 top_p 不会生效,想同时用 beam search 和采样,需要设置 do_sample=True 或者让生成策略明确走 sampling。
4.3 完整示例:长文本到中文摘要
把上面的分句、编码、生成、解码串起来,就是一个完整的摘要流程:
def summarize_long_text(text, tokenizer, model): sentences = split_sentences(text) summaries = [] for sent in sentences: inputs = encode_text(sent, tokenizer, max_len=512) summary_ids = model.generate( inputs["input_ids"], attention_mask=inputs["attention_mask"], max_new_tokens=128, num_beams=4, no_repeat_ngram_size=3, early_stopping=True ) s = tokenizer.decode(summary_ids[0], skip_special_tokens=True) summaries.append(s) return " ".join(summaries) text = "这里是需要摘要的长文本..." result = summarize_long_text(text, tokenizer, model) print(result)这里的思路是每句话生成一个小摘要再拼接。tokenizer.decode 负责把 token id 序列转回文本,skip_special_tokens=True 会把 [CLS]、[SEP]、padding token 这些特殊符号过滤掉,否则摘要里会残留大段的空白符和特殊标记。将每个句子的摘要拼起来后,人工再润色一遍,整篇摘要就能直接用于周报、工单归档这类场景。
如果你直接跑上面的代码显存不够,可以把每句的 max_new_tokens 调小到 64,或者把输入 max_len 降到 256,显存占用会明显下降。还需要确保模型被放到正确的设备上,如果是 GPU 环境,提前执行 model = model.to("cuda"),input_ids 也要随之移到 GPU。要注意的是这种逐句生成方式天然带一个优点:即使某一句生成得不好,也不会影响其他句子的摘要结果,出错面被限制在单句范围内。
5. 常见问题排查:重复、乱码、显存不足的五个现场
5.1 生成结果全是重复文本
现象:生成的摘要里大量出现"我们相信我们相信我们相信"或者同一句话反复循环,长度越生成越不可控,最后输出几百个 token 的复读内容。
原因:自回归模型在解码后期概率分布趋于平滑,会轮流选中几个高概率 token 形成死循环。贪心解码尤其容易触发,beam search 稍好但也会出现跨 beam 的重复。如果模型没有切到 eval 模式,Dropout 的随机性也会加重这种现象。
解决:先加 no_repeat_ngram_size=3 禁止三元组重复;再把 temperature 降到 0.8 左右,让分布更尖锐。如果还不行,把 max_new_tokens 调短,让模型在进入重复区之前就停止。我曾经遇到一次怎么调都复读的情况,排查了半小时才发现是模型忘了切 eval 模式,Dropout 在推理时还在工作,加上这两行之后输出立刻正常了。
5.2 摘要与原文主题脱节
现象:输入的是一篇新能源汽车销量分析,生成的摘要却在讲充电桩布局政策;或者说了一堆原文里根本没有的细节。
原因:一种是输入的文本被 padding 到固定长度后主题词被稀释,模型在生成时注意力分散;另一种是自回归模型顺着自己最近生成的几个词往下跑,逐步偏离原文。抽象式摘要还会产生"幻觉",模型用自己的语言知识补出了原文不存在的事实。
解决:首先要确保 attention_mask 传进了 generate,否则 padding 位置会被当成正常上下文。其次是控制摘要长度,max_new_tokens 太大时模型有足够空间跑偏。更有效的手段是把文章第一句作为引导语拼到 prompt 里,因为新闻、报告的第一句话通常就是主题句,模型续写时有一个更强的锚点。如果任务允许,可以先用 Lead-3 抽取前三句作为基线,再让 GPT-2 做抽象压缩,两个结果对比着用,能明显抑制主题漂移。
5.3 中文乱码与分词错乱
现象:摘要里出现 [UNK] 标记、连续的方块字符,或者"人工智能"被拆成"人工智"和"能"两段,读起来非常别扭。
原因:tokenizer 词表里没有覆盖原文的生僻字,或者加载的 tokenizer 和模型不匹配,比如用了 BERT 的中文词表去配 GPT-2 模型。另一个常见来源是文本截断发生在多字节字符中间,半个字符被单独编码成了乱码 token。
解决:加载模型时,tokenizer 的路径必须和模型路径一致,不要手动指定别的词表文件。生僻字问题可以在 tokenizer.decode 之前做一次 [UNK] 替换,把未知标记映射回原文字符。还有一个工程技巧:在 encode_text 里把 max_length 调大,避免长文本被截断后最后半个中文字被切掉。
5.4 显存不足与推理速度过慢
现象:批量推理时直接报 CUDA out of memory,或者单条新闻摘要要等十几秒,CPU 上尤其明显。
原因:输入长度 512、beam 宽度 4,等于同时跑 4 个候选序列,显存和计算量都翻了 4 倍左右。CPU 环境推理慢是正常的,即使小规模 GPT-2 也有上亿参数,逐 token 生成天然不适合纯 CPU 跑。
解决:显存不够就降 beam,num_beams=1 退回贪心解码,或者把 max_new_tokens 和输入 max_length 都调小。速度慢的话,先确认模型在 GPU 上而不是默认的 CPU,再用半精度加载:model = AutoModelForCausalLM.from_pretrained(model_dir, torch_dtype=torch.float16)。如果还需要更快,可以把整段逻辑包装进 ONNX Runtime,但前提是你不需要频繁改模型结构。
5.5 模型加载失败与版本冲突
现象:from_pretrained 报错说 GPT2LMHeadModel 不在 auto 模型注册表里,或者报 key 不匹配的 warning,最终加载出来的权重形状错误,推理结果完全不可用。
原因:代码包里的 modeling_auto.py 和本地安装的 transformers 版本不一致。老版本 transformers 里 AutoModelForCausalLM 能识别的模型类集合不同,新版本又可能改了注册方式,导致文件里的类名和注册表对应不上。
解决:先确认环境里 transformers 版本,pip show transformers,再看代码包对应版本。最省事的做法是用代码包目录下的文件覆盖安装:把 modeling_*.py 和 tokenization_utils.py 拷到 site-packages/transformers/models/ 对应的子目录里,然后重启 Python 解释器。这里有一个血泪经验:改完建模代码后一定要重启内核,Python 会把模块缓存住,不重启就用不上新代码,我为此翻过好几次车。
6. 进阶:微调、ROUGE 评估与脱敏,让摘要模型真正可用
6.1 用中文数据集微调模型并做效果验证
预训练模型生成的摘要比较泛,特定领域比如周报、工单、医疗新闻想要稳定输出,微调省不掉。微调数据是(原文,参考摘要)对,我用代码包里的 model_config_dialogue_small.json 作为配置起点,加载一个小规模的 GPT-2 模型,用 Trainer 做有监督训练:
from transformers import Trainer, TrainingArguments training_args = TrainingArguments( output_dir="./gpt2_summary_finetuned", per_device_train_batch_size=2, learning_rate=5e-5, num_train_epochs=3, save_strategy="epoch" ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset ) trainer.train()batch size 设 2 是因为摘要序列长,显存容易吃满;学习率 5e-5 是微调语言模型的保守起点,太大容易让模型遗忘预训练学到的中文语法。数据集要处理成模型能读的格式,padding 和 attention mask 的处理必须和第 3 章的 encode_text 保持一致。
微调完不要急着上生产,先用 ROUGE 量化一下。rouge_score 库可以直接算 ROUGE-1、ROUGE-2、ROUGE-L:
from rouge_score import rouge_scorer scorer = rouge_scorer.RougeScorer(["rouge1", "rougeL"], use_stemmer=False) scores = scorer.score("参考摘要", "生成摘要") print(scores["rouge1"].fmeasure)ROUGE 只统计字面重叠,不判断语义是否一致,所以我的习惯是对数值排名靠前的样本再人工抽查 30 到 50 条,数值和体感双重确认后才把模型出包。
6.2 部署前的脱敏与验证习惯
生产环境里的原始文本大多带着手机号、身份证号、人名,我的做法是在进模型之前先替换成 138****1234、[NAME] 这类脱敏形式,生成摘要后再换回可读文本,保证模型不会记忆完整敏感字段。这样从训练到推理的资料都过一遍脱敏检查,已经是我每次出包的固定动作。
部署则用 FastAPI 包一层 HTTP 接口,控制并发和 max_new_tokens 上限,防止单请求占满显存。从那以后,我每次上生产环境前都强制走一遍"分句处理、generate 参数、ROUGE 抽查、脱敏验证"四步流程,踩过的坑基本都堵死了。希望帮到你。
本文还有配套的精品资源,点击获取