ARTICLE DETAIL

资讯详情

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

基于GPT2与Transformers的春节对联自动生成系统实践

基于GPT2与Transformers的春节对联自动生成系统实践 简介基于GPT2模型的春节对联自动生成系统是一套面向深度学习与自然语言处理学习者的实践项目融合transformers库与自定义数据集训练可在春节语境下自动生成对仗工整的中文对联兼具技术学习与传统文化传播双重价值。资源共15个文件涵盖Python训练与测试脚本、预置对联语料、词表、配置文件JSON、运行说明文档等其中txt与docx提供了详细使用指南压缩包整体仅2.52MB轻量易用。目前已有57人学习下载适合想复现GPT2中文生成、研究中文创意文本或对AI写春联感兴趣的开发者和学生。通过该压缩包可拿到从数据预处理、模型训练到生成测试的完整代码与数据并可根据自定义数据集微调模型生成不同风格的对联是快速上手深度学习文本生成的实用参考。1. 春节对联自动生成系统第一版就死在词表上把春节对联自动生成系统当成普通文本生成项目来做第一版基本都会翻车在同一个地方加载了原版 GPT2 权重就直接跑结果中文被切成字节碎片生成结果全是“”。这是我在做这类中文生成项目时见过最多的问题也最能说明系统成败不依赖模型多强而依赖数据、词表和后处理这三层是否能拧成一股绳。标题里的关键词拆开看就是三条明确的工作线用 GPT2 做自回归底座用 transformers 库承载训练与推理用一个自定义对联数据集来微调模型。最终系统要同时满足“能生成上联、能对出下联、能写成整副春节对联”这三个动作而不是像聊天机器人那样自由发散。适合动手做过 BERT 分类但没碰过 CausalLM 生成、或者想在小规模数据集上跑通文本生成全流程的工程师。2. 生成对联之前先拆解中文对仗和平仄约束2.1 对联任务为什么适合自回归生成GPT2 的核心结构是 decoder-only 的 Transformer训练目标是从左到右预测下一个 token。这个性质放在对联任务里非常顺手一副春节对联可以看作“上联 分隔符 下联 结束符”的连续序列。上联作为前缀下联作为要预测的内容模型不需要额外设计 encoder 和 decoder 之间的注意力交互一个模型就能覆盖上联生成、下联对仗、横批补全三种子任务。T5、BART 这类 seq2seq 模型也能做但要同时维护 encoder 对输入的理解和 decoder 对输出的约束在“给定主题词生成上联”“给定上联生成下联”“给定上下联生成横批”这些混合场景里任务形态不一致会让训练目标变复杂。GPT2 把所有子任务统一成“前缀续写”数据整理起来也简单。代价是 GPT2 没有显式的对仗约束注意力也只能从左往右看生成下联第 N 个字时看不到下联后文所以“语义对得上”这件事要靠训练数据里足够多的平行对来学习而不是靠模型结构保证。这就是为什么后面必须要加规则校验而不是指望模型自己学会平仄。自回归结构决定了下联生成是一个逐步贪心展开的过程温度、重复惩罚这些采样参数才是系统里真正可控的调节阀第 5 章会专门展开。2.2 中文词表选择为什么不直接加载原版 GPT2原版 GPT2 训练用的是 BPE 词表这个词表对英文很友好但覆盖的中文字符极少。把“爆竹声声辞旧岁”输入进去会被切成一堆 byte-level 片段训练时每个字要拼回好几个 subword收敛慢且生成质量差。中文项目里更务实的选择是用 BERT 的中文词表来代替常见做法是加载bert-base-chinese的 tokenizer配合 GPT2LMHeadModel 使用。这里有一个必须处理的坑BertTokenizer默认词表大小是 21128而 GPT2 预训练模型的 embedding 层是按英文字表初始化的。直接拼在一起会报维度不匹配或者干脆解码成[UNK]。我一般会这样做先把 tokenizer 加载好用len(tokenizer)拿到词表大小然后调用model.resize_token_embeddings(len(tokenizer))扩展 embedding。这一步不做后面的训练脚本十有八九会在输入阶段或者保存权重时报错。2.3 平仄、对仗与字数约束怎么进入训练样本平仄不是模型结构问题是数据频率问题。一副联里“上联末字仄声、下联末字平声”这个规律只有大量出现在训练数据里GPT2 才能从统计上学进去。所以训练样本不能只存“上联下联”两行原文字段还要做统一的格式加工。我用[SEP]做上下联之间的分隔用[EOS]做整副联的结束训练文本统一为“上联 [SEP] 下联 [EOS]”。不推荐用换行符分隔因为中文 tokenizer 可能会把换行处理成特殊 token推理时生成的换行也会干扰输出解析。每副对联在训练时只保留一条数据重复的对联如果出现在数据里模型会直接背诵而不是学习对仗规律。样本形如上联爆竹声声辞旧岁[SEP]下联梅花朵朵迎新春[EOS]这条数据进入训练之前我会先做一层过滤上下联字数不一致的剔除长度超过 16 字一副的剔除因为春节常用七言和五言含繁体字的降级处理保简体为主否则声调判断在推理阶段容易乱。这些清洗规则看起来琐碎但它们决定了模型生成结果是不是能直接挂出去用。3. 用 transformers 库搭建数据集与训练环境3.1 解压 Tensor.zip 后我先核对三样东西项目交付包如果叫 Tensor.zip里面通常不会只有一个模型文件。我拿到手后的第一步不是加载权重而是核对配置顺序如下解压、确认目录结构、读取 config.json、核对 tokenizer 词表长度。unzip Tensor.zip -d couplet_sys cd couplet_sys python -m json.tool config.json | grep vocab_size python -c from transformers import BertTokenizer; t BertTokenizer.from_pretrained(./tokenizer); print(len(t))重点比对这个数字和vocab_size是否一致。如果不一致加载后模型编码根本对不上。常见做法是先让 tokenizer 自己建好词表再根据真实长度 resize 模型后面训练代码里我会演示。还要看 config.json 里有没有model_type字段确认加载路径走的是 GPT2LMHeadModel 而不是 BertForMaskedLM这两个模型的名字相近但输出头完全不同。3.2 对联数据集清洗规则与 JSONL 格式自定义数据集采用 JSONL 格式每行一个 JSON 对象只保留上联和下联两个原始字段后续处理程序里再来组装训练文本。清洗脚本我一般这样写import json import re def filter_couplet(line: str): obj json.loads(line.strip()) upper, lower obj[上联], obj[下联] if len(upper) ! len(lower): return None if not re.fullmatch(r[\u4e00-\u9fff。、\-]{5,16}, upper lower): return None return obj with open(couplets.jsonl, r, encodingutf-8) as fin, \ open(couplets_clean.jsonl, w, encodingutf-8) as fout: for line in fin: cleaned filter_couplet(line) if cleaned: fout.write(json.dumps(cleaned, ensure_asciiFalse) \n)正则里只保留中文字符和常用标点限制在五言到十六言之间这能过滤掉大量杂乱的网页抓取数据。这一步做完一般能去掉一半以上的样本剩下的质量仍参差但比直接喂原文好得多。数据集规模不需要很大几千副质量过关的对联已经足以微调一个小型 GPT2 模型。3.3 tokenizer、模型与 data collator 三件套怎么配合实现阶段的核心组件有三个tokenizer、GPT2LMHeadModel、以及自定义的 data collator。tokenizer 负责把文本转成 id 序列模型负责自回归计算collator 负责把不定长序列对齐成 batch。from transformers import GPT2LMHeadModel, BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) model GPT2LMHeadModel.from_pretrained(uer/gpt2-chinese-cluecorpussmall) model.resize_token_embeddings(len(tokenizer)) tokenizer.pad_token tokenizer.eos_token第二行的加载路径选的是中文语料预训练的 GPT2 权重比直接加载英文gpt2更适合这个任务。resize_token_embeddings是这里最容易被忽略的一步模型加了新词表后 embedding 矩阵是随机初始化的训练时新词表部分会被专门锻炼这正好符合对联数据集里大量控制符号的场景。构造训练样本时上联部分的 label 全部置为 -100让模型只学习下联部分的输出。原因很简单如果上下联都计算 loss模型只要学会复读上联就能把训练损失降到很低但完全没有对仗能力。collator 里同样要对 label 做 padding填充值为 -100这个值在 transformers 的 loss 计算中被自动忽略。4. 用 GPT2 训练自定义对联数据集脚本、参数与调试4.1 PyTorch 侧的最小训练脚本数据准备和训练入口分开写dataset 负责把清洗后的 JSONL 转成输入import torch from torch.utils.data import Dataset class CoupletDataset(Dataset): def __init__(self, path: str, tokenizer, block_size: int 128): self.examples [] with open(path, r, encodingutf-8) as f: for line in f: obj json.loads(line.strip()) upper obj[上联] lower obj[下联] up_ids tokenizer.encode(upper, add_special_tokensFalse) low_ids tokenizer.encode(lower, add_special_tokensFalse) all_ids up_ids [tokenizer.sep_token_id] low_ids [tokenizer.eos_token_id] labels [-100] * len(up_ids) low_ids [tokenizer.eos_token_id] all_ids all_ids[:block_size] labels labels[:block_size] self.examples.append({input_ids: all_ids, labels: labels}) def __len__(self): return len(self.examples) def __getitem__(self, i): return {k: torch.tensor(v, dtypetorch.long) for k, v in self.examples[i].items()}add_special_tokensFalse很关键因为手动拼接了[SEP]和[EOS]如果默认再加一次 special token序列会多出一份重复的[CLS]开头。上联部分 label 为 -100下联和结束符保留原始 id模型只能从下联部分学到有效梯度。训练循环用 transformers 的Trainer封装不自己写 backwardfrom transformers import Trainer, TrainingArguments, DataCollatorForLanguageModeling data_collator DataCollatorForLanguageModeling( tokenizertokenizer, mlmFalse ) args TrainingArguments( output_dir./couplet_gpt2, per_device_train_batch_size16, num_train_epochs8, learning_rate5e-5, warmup_steps200, logging_steps50, save_steps500, fp16torch.cuda.is_available(), ) trainer Trainer( modelmodel, argsargs, train_datasetds, data_collatordata_collator, ) trainer.train()DataCollatorForLanguageModeling在mlmFalse时只做 padding 和 label 对齐不会随机 mask适合 GPT2 这种自回归模型的微调场景。训练参数里save_steps500的节奏可以在每个 epoch 内多留几个存档点方便回滚到 loss 最低的那一步而不是依赖最后一个 epoch 的权重。4.2 TensorFlow 2.x 的等效做法标题里带 Tensor 并不代表一定得用 TensorFlow但如果部署环境要求 TF 版本transformers 也提供了对应封装。PyTorch 训练好的权重可以用save_pretrained()保存之后用TFAutoModelForCausalLM.from_pretrained()加载权重会自动转换。from transformers import TFAutoModelForCausalLM tf_model TFAutoModelForCausalLM.from_pretrained(./couplet_gpt2)要注意 TF 侧没有resize_token_embeddings的完全等价物词表对齐必须在 PyTorch 侧做。我的做法是先在 PyTorch 环境里完成词表检查、数据训练和权重保存TF 只负责推理部署。训练阶段跨框架切换没有收益反而会因为 embedding 初始化不一致引入不必要的 bug。4.3 关键参数表与训练曲线判读训练阶段最值得调的是下面这几个参数参数推荐范围说明异常信号num_train_epochs5 ~ 10数据集小epoch 少了欠拟合多了背诵训练集loss 下降但生成重复度升高learning_rate3e-5 ~ 1e-4微调场景用 5e-5 起步loss 震荡或 NaN 说明 lr 过高per_device_train_batch_size8 ~ 24取决于显存序列长度 128 时 16 够用OOM 时减半warmup_steps100 ~ 300让学习率从 0 起步稳定早期训练loss 刚开始剧烈波动fp16开/关显存不够时开启A100 以下要看 loss 是否漂移loss 反复跳动可关掉训练曲线不能只看最终 loss。对联任务里 loss 在 0.3 以下就要小心模型可能靠背诵高频对联把指标刷下来了。这时候需要做一次生成测试看结果里是不是大量出现训练集中的原句。epoch 并不是越大越好对联这种格式高度统一的文本任务超过 10 个 epoch 后模型会趋向“模板化”上联变了一点下联还是那几句老话。4.4 训练失败时最常见的三类排查路径第一类是显存不足。报错通常是 CUDA out of memory处理优先级是batch_size减半再叠加gradient_accumulation_steps4保持等效 batch 大小最后再考虑fp16和降低block_size。顺序不能反先调结构后再开精度反而容易掩盖真正瓶颈。第二类是推理时 tokenizer 警告。报错文案会出现 “The input ids are automatically padded” 这类提示说明数据 collator 没有正确生成attention_mask。检查点有两个tokenizer 的pad_token是否设置以及训练时传入DataCollatorForLanguageModeling是否在同一个 tokenizer 实例上构造。第三类是 loss 不降。排除 lr 和学习率调度原因后重点看样本质量。比如之前清洗时留下大量字数不一致的数据模型在学习一个不可能学会的映射loss 自然降不下去。用开头那套清洗正则重新过滤一遍比改模型结构快得多。5. 生成质量校验、平仄验证与快速部署技巧5.1 温度、top_p 与重复惩罚组合训练好的模型直接采样输出经常是“爆竹声声辞旧岁爆竹声声迎新春”这种原地重复。GPT2 生成对联时我把采样参数固定成下表这套组合参数值作用temperature0.8降低分布锐度减少随机噪声top_p0.85只从累计概率前 85% 的 token 里采样repetition_penalty1.3对已出现 token 打分打折no_repeat_ngram_size3禁止连续三个字重复最后两个参数对联尤其重要。对联本身字数少模型生成过程中很容易连续出现“春春春”这类低质量片段repetition_penalty配合no_repeat_ngram_size能把这类输出直接压掉。温度不要超过 1.0我在测试里 1.2 以上时下联开始出现明显跑题平仄校验通过率也掉得厉害。5.2 平仄校验函数与批量评估这套系统要支持传统文化传播就不能只输出文本不校验声调。我写了一个轻量校验函数用 pypinyin 取每个字的声调数字再按对联规则做判断from pypinyin import pinyin, Style def get_tone(ch: str) - int: try: return int(pinyin(ch, styleStyle.TONE3, heteronymFalse)[0][0][-1]) except (IndexError, ValueError): return 0 def validate_couplet(upper: str, lower: str): if len(upper) ! len(lower): return False, f字数不一致: {len(upper)} vs {len(lower)} up_end get_tone(upper[-1]) low_end get_tone(lower[-1]) if up_end not in (3, 4): return False, f上联末字应为仄声当前调值 {up_end} if low_end not in (1, 2): return False, f下联末字应为平声当前调值 {low_end} for i, (u, l) in enumerate(zip(upper, lower)): if get_tone(u) ! 0 and get_tone(u) get_tone(l): return False, f第 {i1} 字平仄未相对 return True, 平仄校验通过批量评估时取一批验证集上联逐个生成下联再调这个函数统计通过率。我一般要求通过率至少 80% 才把模型放出实测。校验函数只处理声调对仗的部分交给人工抽检因为语义层面的对仗自动化成本太高不值得在 MVP 阶段做。5.3 把生成接口快速包起来最后一步是把模型封装成可调用的服务。用 FastAPI 包一个最小接口生成逻辑放独立函数里校验结果一起返回from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class Req(BaseModel): upper: str app.post(/generate) def generate(req: Req): prompt f上联{req.upper}[SEP]下联 inputs tokenizer(prompt, return_tensorspt) out model.generate(**inputs, max_new_tokens16, temperature0.8, top_p0.85, repetition_penalty1.3) lower tokenizer.decode(out[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue) ok, msg validate_couplet(req.upper, lower) return {upper: req.upper, lower: lower, valid: ok, msg: msg}调试时可以先在命令行跑通生成函数再接 FastAPI。上线前把这个带校验结果的接口放到内网供同事抽测一批通过率稳定在 80% 以上再放量传统文化传播类应用的生成结果审核成本就会低很多。本文还有配套的精品资源点击获取
返回列表