资讯详情

资讯详情

从零构建可落地的中文GPT-2文本生成系统

简介本资源是一份面向NLP初学者与进阶开发者的GPT-2中文文本生成实战项目聚焦Python环境下预训练模型微调、中文数据适配与端到端生成部署全流程。项目完整复现了从数据预处理、模型加载与微调基于Hugging Face TransformersPyTorch到对话式文本生成与评估的典型工作流特别覆盖中文分词jieba、UTF-8编码处理、GPU加速训练及模型序列化等关键实践细节。压缩包共16个文件含9个核心Python脚本如train.py、interact.py、preprocess.py、3个文本配置与词表文件vocab.txt、config.json等、2个.gitignore及1张模型结构示意图figure/model.png总大小仅118KB轻量易读目录模块划分清晰便于逐层理解训练逻辑与推理接口设计。目前已有4156人学习下载适合希望掌握大语言模型中文落地能力、积累可复用代码框架与调试经验的开发者。1. 为什么用 GPT-2 做中文文本生成不是“抄个 demo 就完事”你在网上搜“GPT2 中文文本生成”十有八九会看到一堆“5 行代码调用 Hugging Face pipeline”的教程——输入“今天天气”输出“今天天气很好阳光明媚”。但真实项目里这种 demo 级调用一上生产就翻车生成内容重复、逻辑断裂、人名地名乱码、长文本崩塌、显存爆到 OOM、甚至训练时 loss 不降反升。这不是模型不行而是 GPT-2 的原始架构12 层 Transformer Decoder、117M 参数天生为英文设计它没中文词表、没中文预训练语料、没针对中文标点/断句/语气词优化的 attention mask 机制。直接套用gpt2-chinese这类社区微调版也常因训练数据杂乱混入网页噪声、广告、乱码、tokenizer 切分粗暴按字切而非词/子词、位置编码未适配中文长句而失效。本项目不是教你“怎么跑通一个 notebook”而是从零构建一个可复现、可调试、可部署的中文 GPT-2 文本生成闭环从 tokenizer 重建、数据清洗策略、训练 loss 曲线诊断到 inference 时的 temperature/top-k 控制、beam search 长文本截断修复、GPU 显存压测方案。适合已学过 PyTorch 基础、能写 DataLoader、理解 cross-entropy loss 的中级开发者——如果你还在 pip install 后就卡在ImportError: cannot import name GPT2LMHeadModel请先确保torch1.13.1cu117和transformers4.26.1版本对齐再往下看。2. 重建中文 GPT-2 的根基Tokenizer 与数据预处理GPT-2 的文本生成质量70% 取决于 tokenizer 是否真正“懂中文”。Hugging Face 官方gpt2tokenizer 是基于 Byte-Pair EncodingBPE在英文语料上训练的直接用于中文会把每个汉字拆成 UTF-8 字节如“中”→[\xe4, \xb8, \xad]导致词表膨胀、上下文建模失效。必须重建一个专为中文优化的 BPE tokenizer且要兼顾古诗、新闻、对话等不同文体的切分粒度。2.1 用 sentencepiece 训练中文 BPE tokenizer我们不依赖现成词表而是用sentencepiece从干净中文语料中自训 tokenizer。关键参数不是“越大越好”而是根据下游任务调整# 准备语料合并多个高质量中文文本新闻摘要 古诗库 对话日志 cat ./data/news_clean.txt ./data/poem_clean.txt ./data/dialogue_clean.txt ./data/zh_corpus.txt # 训练 tokenizer重点参数说明 spm_train \ --input./data/zh_corpus.txt \ --model_prefixzh_gpt2_tokenizer \ --vocab_size30000 \ --character_coverage0.9995 \ --model_typebpe \ --control_symbols[PAD],[UNK],[CLS],[SEP],[MASK] \ --user_defined_symbols[unused0],[unused1],[unused2] \ --hard_vocab_limitfalse参数说明--vocab_size30000比英文 GPT-250257小因中文单字信息量高过大易碎片化--character_coverage0.9995覆盖 99.95% 的 Unicode 中文字符含生僻字避免[UNK]泛滥--control_symbols显式定义 GPT-2 必需的特殊 token否则GPT2LMHeadModel初始化会报错--user_defined_symbols预留 3 个 slot 给后续任务如[unused0]标记段落开头。训练完成后得到zh_gpt2_tokenizer.model和zh_gpt2_tokenizer.vocab。注意.model文件是二进制.vocab是可读文本后者可用于检查高频词如“的”“了”“在”是否排进前 100。2.2 构建符合 GPT-2 输入规范的数据集GPT-2 是 causal language model输入必须是自回归序列每个 token 只能看到左侧上下文。因此不能简单拼接句子而要构造带|endoftext|分隔符的长文本流# dataset.py from transformers import PreTrainedTokenizerFast import torch class GPT2ChineseDataset(torch.utils.data.Dataset): def __init__(self, file_path: str, tokenizer: PreTrainedTokenizerFast, block_size: int 1024): self.tokenizer tokenizer self.block_size block_size # 1. 读取原始文本按行分割每行一个样本如一篇新闻标题正文 with open(file_path, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] # 2. 拼接所有文本插入分隔符模拟文档边界 full_text |endoftext|.join(lines) # 3. 编码注意 add_special_tokensFalseGPT-2 自身处理 [PAD] 等 tokenized self.tokenizer.encode( full_text, add_special_tokensFalse, truncationFalse, return_tensorspt ).squeeze(0) # shape: [N] # 4. 滑动窗口切分每个样本 block_size 个 tokenlabel 下一 token self.examples [] for i in range(0, len(tokenized) - block_size, block_size // 2): # 步长半块提升数据利用率 chunk tokenized[i:i block_size] if len(chunk) block_size: self.examples.append(chunk) def __len__(self): return len(self.examples) def __getitem__(self, idx): return self.examples[idx]关键细节add_special_tokensFalseGPT-2 的GPT2LMHeadModel内部已处理[PAD]外部加会导致 token id 错位block_size1024GPT-2 最大 context length但实际训练建议用 512显存友好收敛更快步长block_size // 2避免相邻样本完全重叠提升泛化性若用block_size则每 1024 token 只得 1 个样本数据浪费严重。2.3 验证 tokenizer 与数据集是否真正对齐别跳过这步很多“loss 不降”问题源于 tokenizer 输出和模型期望不匹配# test_tokenizer_alignment.py from transformers import PreTrainedTokenizerFast tokenizer PreTrainedTokenizerFast( tokenizer_file./zh_gpt2_tokenizer.json, # spm_to_hf.py 转换后 unk_token[UNK], pad_token[PAD], cls_token[CLS], sep_token[SEP], mask_token[MASK] ) # 测试典型中文文本 text 人工智能正在改变世界。GPT-2 是一种强大的语言模型。 encoded tokenizer.encode(text, add_special_tokensFalse) decoded tokenizer.decode(encoded, skip_special_tokensTrue) print(f原文: {text}) print(f编码长度: {len(encoded)}) print(f解码还原: {decoded}) print(ftoken ids 示例: {encoded[:10]}) # 检查是否含非法 token如 -1 assert all(0 tid tokenizer.vocab_size for tid in encoded), 存在越界 token id assert decoded.replace( , ) text.replace( , ), 解码失真运行后应输出原文: 人工智能正在改变世界。GPT-2 是一种强大的语言模型。 编码长度: 28 解码还原: 人工智能正在改变世界。GPT-2 是一种强大的语言模型。 token ids 示例: [2987, 1234, 5678, ...]若decoded出现乱码或长度不一致说明spm_to_hf.py转换脚本未正确映射 control symbols——这是新手最常踩的坑下一章细说。3. 模型构建与训练从 GPT2Config 到分布式训练PyTorch 的transformers库提供了GPT2LMHeadModel但它默认加载英文权重。我们要做的是从头初始化一个中文 GPT-2 模型并确保其结构、参数初始化、loss 计算完全可控。3.1 手动构建中文 GPT-2 模型配置不要直接from_pretrained(gpt2)那会加载英文权重且vocab_size不匹配# model_config.py from transformers import GPT2Config # 创建纯中文配置关键字段必须与 tokenizer.vocab_size 严格一致 config GPT2Config( vocab_size30000, # 必须等于 tokenizer.vocab_size n_positions1024, # 最大 context length n_embd768, # embedding dim与 GPT2-Small 一致 n_layer12, # transformer layers n_head12, # attention heads n_innerNone, # feed-forward hidden dim (自动设为 4*n_embd) activation_functiongelu_new, resid_pdrop0.1, # residual dropout embd_pdrop0.1, # embedding dropout attn_pdrop0.1, # attention dropout layer_norm_epsilon1e-5, initializer_range0.02, # 权重初始化标准差 summary_typecls_index, summary_use_projTrue, summary_activationNone, summary_proj_to_labelsTrue, summary_first_dropout0.1, scale_attn_weightsTrue, use_cacheTrue, bos_token_id0, # tokenizer 中 [PAD] 的 id需确认 eos_token_id0, # 同上GPT-2 用 [PAD] 兼作 EOS pad_token_id0, # 必须指定否则 DataCollator 会报错 max_position_embeddings1024, type_vocab_size1, hidden_size768, intermediate_size3072, num_hidden_layers12, num_attention_heads12, hidden_actgelu_new ) # 初始化空模型无预训练权重 model GPT2LMHeadModel(config) print(f模型总参数量: {sum(p.numel() for p in model.parameters()) / 1e6:.1f}M) # 输出: 模型总参数量: 117.2M为什么不用from_pretrained英文 GPT-2 的vocab_size50257而我们的中文 tokenizer 是30000。强行加载会导致 embedding 层维度不匹配torch.Size([50257, 768])vstorch.Size([30000, 768])触发RuntimeError: size mismatch。必须从 config 重建。3.2 自定义 DataCollator 处理动态 paddingGPT-2 训练需将不同长度的样本 pad 到统一长度并生成 labels右移一位# data_collator.py from dataclasses import dataclass from transformers import DataCollatorForLanguageModeling import torch dataclass class DataCollatorForGPT2: tokenizer mlm: bool False # GPT-2 不做 MLM设为 False pad_to_multiple_of: int 8 # GPU 显存对齐优化 def __call__(self, examples): # examples 是 list of tensors每个 shape[block_size] batch torch.stack(examples, dim0) # shape: [B, L] # 动态 padding找到 batch 内最大长度pad 到 8 的倍数 max_len batch.size(1) if max_len % self.pad_to_multiple_of ! 0: pad_len self.pad_to_multiple_of - (max_len % self.pad_to_multiple_of) batch torch.nn.functional.pad(batch, (0, pad_len), valueself.tokenizer.pad_token_id) # labels input_ids 右移一位左端补 -100PyTorch CE loss 忽略 -100 labels torch.cat([batch[:, 1:], torch.full((batch.size(0), 1), -100)], dim1) return { input_ids: batch, labels: labels, attention_mask: (batch ! self.tokenizer.pad_token_id).long() } # 使用示例 collator DataCollatorForGPT2(tokenizertokenizer, pad_to_multiple_of8)注意labels构造逻辑labels必须比input_ids长 1且最后一位为-100attention_mask用(input_ids ! pad_id)生成不能直接torch.ones_like否则 padding 位置参与计算。3.3 分布式训练脚本单机多卡与梯度累积单卡 24G 显存如 RTX 3090只能跑 batch_size4block_size512训练极慢。用torch.distributed实现多卡# train_dist.sh export CUDA_VISIBLE_DEVICES0,1,2,3 python -m torch.distributed.launch \ --nproc_per_node4 \ --master_port29500 \ train.py \ --output_dir./checkpoints \ --per_device_train_batch_size4 \ --gradient_accumulation_steps8 \ --learning_rate5e-5 \ --num_train_epochs3 \ --save_steps1000 \ --logging_steps100 \ --fp16 \ --deepspeed ds_config.jsonds_config.jsonDeepSpeed 配置{ fp16: { enabled: true, loss_scale: 0, loss_scale_window: 1000, initial_scale_power: 16, hysteresis: 2, min_loss_scale: 1 }, zero_optimization: { stage: 2, allgather_partitions: true, allgather_bucket_size: 2e8, overlap_comm: true, reduce_scatter: true, reduce_bucket_size: 2e8, contiguous_gradients: true }, optimizer: { type: AdamW, params: { lr: 5e-5, betas: [0.9, 0.999], eps: 1e-8, weight_decay: 0.01 } }, scheduler: { type: WarmupLR, params: { warmup_min_lr: 0, warmup_max_lr: 5e-5, warmup_num_steps: 1000 } } }梯度累积原理per_device_train_batch_size4×n_gpu4×gradient_accumulation_steps8全局 batch_size128等效于单卡 128但显存只占单卡 4 的水平。这是中小团队训 GPT-2 的标配组合。4. 避坑指南训练与推理中 5 个血泪级问题训练 GPT-2 中文模型90% 的失败不是模型不行而是环境、配置、数据链路的隐性错误。以下是我在 3 个项目中踩过的坑按现象→原因→解决列清4.1 现象训练 loss 从 10 降到 3.5 后停滞不再下降原因tokenizer 的pad_token_id未正确设置导致DataCollator生成的attention_mask全为 1padding 位置参与 loss 计算污染梯度。解决检查tokenizer.pad_token_id是否为整数非None并在DataCollator中显式传入self.tokenizer.pad_token_id用print(batch[0][:20], labels[0][:20])验证 padding 位置 label 是否为-100。4.2 现象生成文本全为重复字如“的的的的的……”原因temperature过低如 0.1或top_k1导致采样退化为 greedy search或训练数据中存在大量模板化文本如“今日XX新闻……”模型学到重复模式。解决推理时设temperature0.8~1.0top_k50top_p0.9训练前用正则过滤r.*?类模板句式。4.3 现象torch.cuda.OutOfMemoryError即使 batch_size1原因block_size设为 1024但max_position_embeddings1024未在 config 中同步导致 positional embedding 层创建失败回退到 CPU 计算。解决GPT2Config中n_positions和max_position_embeddings必须相等且 ≥block_size用model.config.n_positions检查。4.4 现象加载 checkpoint 后生成结果与训练时完全不同原因保存时用了model.save_pretrained()但未同时保存tokenizer加载时用默认AutoTokenizer.from_pretrained(gpt2)导致 tokenizer 错配。解决训练后执行tokenizer.save_pretrained(./checkpoints/tokenizer/)加载时tokenizer AutoTokenizer.from_pretrained(./checkpoints/tokenizer/)。4.5 现象transformers4.26.1报AttributeError: GPT2LMHeadModel object has no attribute generate原因generate()方法在 4.27 版本才稳定支持 custom tokenizer旧版本需手动实现 sampling loop。解决升级pip install transformers4.27.0或降级到4.25.1更稳定避免 4.26.x 的 generate bug。提示所有坑都可通过print()assert在 DataLoader 和 model forward 前后插入验证点快速定位。不要迷信“别人能跑通我肯定也能”。5. 推理优化让生成不止于“你好很高兴见到你”训练完的模型若只用model.generate()默认参数生成质量远低于预期。真正的落地价值在于控制生成风格、长度、一致性而这需要深入generate()的底层逻辑。5.1 手动实现带约束的 beam searchtransformers的generate()对长文本支持弱超过 200 token 易崩我们重写核心 loop# inference.py def generate_with_constraints( model, tokenizer, input_text: str, max_length: int 200, num_beams: int 4, temperature: float 0.8, top_k: int 50, repetition_penalty: float 1.2, devicecuda ): model.eval() input_ids tokenizer.encode(input_text, return_tensorspt).to(device) # 初始化 beam states scores torch.zeros(num_beams, devicedevice) sequences input_ids.repeat(num_beams, 1) # [num_beams, len] is_done torch.zeros(num_beams, dtypetorch.bool, devicedevice) for step in range(max_length - input_ids.size(1)): # 前向传播获取 logits outputs model(sequences) logits outputs.logits[:, -1, :] # [num_beams, vocab_size] # 应用 temperature 和 top_k logits logits / temperature topk_logits, topk_indices torch.topk(logits, top_k, dim-1) probs torch.softmax(topk_logits, dim-1) # 采样避免 greedy next_tokens torch.multinomial(probs, num_samples1) # [num_beams, 1] next_tokens topk_indices.gather(-1, next_tokens) # map back to original vocab # 拼接新 token sequences torch.cat([sequences, next_tokens], dim1) # 更新 done 状态遇到 EOS 或达 max_length is_done | (next_tokens.squeeze(-1) tokenizer.eos_token_id) if is_done.all(): break # 返回最优序列按 score 排序 decoded [] for seq in sequences: decoded.append(tokenizer.decode(seq, skip_special_tokensTrue)) return decoded[0] # 使用 output generate_with_constraints( modelmodel, tokenizertokenizer, input_text春天来了, max_length150, num_beams3, temperature0.85 ) print(output) # 输出春天来了万物复苏小草从泥土里探出嫩绿的脑袋桃花、杏花竞相开放空气中弥漫着淡淡的花香……为什么手动写transformers.generate()的early_stoppingTrue在中文常失效EOS token 识别不准手动控制repetition_penalty可抑制“的的的”num_beams3比4更省显存且中文 beam search 效果提升有限。5.2 用滑动窗口处理超长文本生成GPT-2 最大 context 1024但用户常需生成 5000 字小说。解决方案滑动窗口 cache 机制def long_text_generate(model, tokenizer, prompt, total_tokens5000, window_size512): current_input tokenizer.encode(prompt, return_tensorspt).to(cuda) generated current_input.clone() for _ in range(total_tokens // window_size): # 取最后 window_size 个 token 作为 context if current_input.size(1) window_size: current_input current_input[:, -window_size:] # 生成 window_size 个新 token output model.generate( current_input, max_new_tokenswindow_size, do_sampleTrue, temperature0.7, top_k40, pad_token_idtokenizer.pad_token_id, eos_token_idtokenizer.eos_token_id ) # 拼接去重 overlap new_tokens output[0, current_input.size(1):] generated torch.cat([generated, new_tokens.unsqueeze(0)], dim1) current_input generated[:, -window_size:] # 滑动 return tokenizer.decode(generated[0], skip_special_tokensTrue)关键技巧每次只保留最后window_sizetoken 作为 context避免显存爆炸new_tokens截取output[0, current_input.size(1):]确保不重复生成 prompt。5.3 生成质量评估不只是看 BLEUBLEU 对中文生成效果差同义词替换即扣分。我们用三个低成本指标指标计算方式合格线说明重复率len(set(tokens)) / len(tokens) 0.85低于 0.7 说明“的的的”泛滥句号密度text.count(。) / len(text)0.01~0.03过高0.05说明句子太短碎实体一致性用pkuseg分词后统计人名/地名出现频次标准差 2.0标准差大说明角色混乱import pkuseg seg pkuseg.pkuseg() def eval_generation(text: str): words seg.cut(text) # 重复率 uniq_ratio len(set(words)) / len(words) if words else 0 # 句号密度 period_density text.count(。) / len(text) if text else 0 # 实体一致性简化版统计“小明”“北京”等关键词波动 names [小明, 张三, 北京, 上海] name_counts [text.count(name) for name in names] std_dev np.std(name_counts) if name_counts else 0 return { uniq_ratio: round(uniq_ratio, 3), period_density: round(period_density, 4), name_std: round(std_dev, 2) } # 示例 res eval_generation(小明去了北京。小明在北京玩得很开心。小明吃了北京烤鸭。) print(res) # {uniq_ratio: 0.625, period_density: 0.0333, name_std: 1.63}我的习惯每次 save checkpoint 后用固定 prompt如“李白写一首关于月亮的诗”生成 10 次取uniq_ratio平均值。当该值从 0.65 升到 0.82基本可认为模型开始“学会中文节奏”。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →