Hugging face微调 GPT-2模型
hugging face微调GPT-2
一、模型微调的分类
我们现在常用一种基于参数更新范围和资源消耗的分类方式。下图清晰地展示了这三种核心微调方法的参数更新范围与资源需求的平衡关系:

1.1 全量微调
这是最直接、最传统的微调方式。
- 核心思想:不冻结预训练模型的任何部分,在新的下游任务数据集上,对模型所有层、所有权重参数进行再训练。
- 工作原理:
- 加载一个预训练模型(如
BERT,GPT-3)。 - 准备你的特定任务数据(如医疗文本、法律条款)。
- 像训练新模型一样,用新数据对整个网络进行训练,但学习率通常设得很低(例如
1e-5到1e-6),以免“破坏”预训练阶段学到的通用知识。
- 加载一个预训练模型(如
- 优点:
- 潜力最大:如果新数据集与预训练数据分布差异较大且数据量充足,
FFT有可能达到最高的性能上限,因为模型的所有参数都针对新任务进行了优化。 - 从调理论上来讲,全量微调效果要比增量微调效果好。
- 潜力最大:如果新数据集与预训练数据分布差异较大且数据量充足,
- 缺点:
- 计算成本高:需要更新数十亿甚至数百亿的参数,需要大量的 GPU 显存和计算时间。
- 存储成本高:每个微调后的任务都需要保存一份完整的模型副本,对于大模型来说非常占用存储空间。
- 灾难性遗忘:模型可能会过度适应新任务,从而忘记在预训练阶段学到的通用语言知识。
- 全量微调存在效果变差的情况。
- 适用场景:
- 计算资源极其充足。
- 下游任务与预训练任务差异很大,且拥有足够大的高质量领域数据集。
本节课的
GPT-2属于全量微调。
1.2 参数高效微调
参数高效微调(Parameter-Efficient Fine-Tuning, PEFT)是目前针对大语言模型微调的最主流、最热门的方法。可以将它理解为一种高度优化的“增量微调”。其核心思想是:冻结预训练模型的所有参数,仅引入并训练一小部分额外的参数,从而高效地适配下游任务。
- 工作原理:基座模型本身保持不变,通过训练新增的少量参数(如适配器或低秩矩阵)来学习新任务。
- 常见技术:
LoRA(Low-Rank Adaptation):在模型的注意力层旁,注入一个低秩矩阵来模拟参数更新的过程。只训练这些很小的矩阵,效果极好,是目前最流行的 PEFT 方法。Adapter:在Transformer层的内部插入一个小的前馈神经网络模块。只训练这些适配器,而冻结原有模型。Prompt Tuning:不修改模型结构,只在输入层加入一些可训练的“软提示”向量,通过调整这些向量来引导模型产生期望的输出。
- 优点:
- 极高的计算和存储效率:只需训练原模型参数量的
0.01%~1%,大大降低了显存需求和训练时间。 - 避免灾难性遗忘:由于原模型参数被冻结,通用知识得以完好保留。
- 模块化和便携性:可以为不同任务训练多个小的 LoRA 或 Adapter 模块,在推理时快速切换,而无需加载整个大模型。
- 极高的计算和存储效率:只需训练原模型参数量的
- 适用场景:
- 绝大多数大模型微调场景,特别是在计算资源有限的情况下。
- 需要快速为模型适配多个不同任务的场景。
上篇文章的
BERT分类任务的微调属于参数高效微调(增量微调)。从理论上来讲增量微调的效果是比不了全量微调和局部微调。
1.3 局部微调
这是一种介于全量微调和 PEFT 之间的方法,也更符合“局部”的字面意思。
- 核心思想:冻结预训练模型的一部分层,只对另一部分层进行训练。
- 工作原理:
- 冻结底部层,微调顶部层:这是一种常见策略。预训练模型的底层通常学习的是通用特征(如语法、基础语义),而顶层更偏向于特定任务。冻结底层,只微调顶部的若干层,可以在节省资源的同时实现任务适配。
- 只微调特定模块:例如,在 Transformer 模型中,只微调所有的“注意力”层,而冻结前馈神经网络层。
- 优点:
- 相比
FFT,显著降低了计算和存储成本。 - 相比
PEFT,方法更直接,不需要引入新的复杂结构。 - 局部微调一定是有效的,微调的效果取决于数据的质量
- 相比
- 缺点:
- 需要人工判断冻结哪些层、训练哪些层,这需要一定的领域知识和实验。
- 效率和性能通常不如
PEFT方法(如LoRA)。
- 适用场景:
- 资源有限,但又不想或不能使用 PEFT 方法的情况。
- 当你知道模型的某些部分(如顶层)对你的任务特别重要时。
我们前面的 BERT 微调其实有两种做法:
-
在
BERT模型的输出层增加一层下游任务层,所以这种方式叫增量微调; -
修改最后一层的输出层,将它改为输出2个特征。直接拿
BERT来做训练,把模型拆开,将模型最后一层前面的参数冻结,保留最后一层不冻结,这种叫局部微调;
1.4 总结与对比
| 特性 | 全量微调 | 参数高效微调(PEFT) | 局部微调 |
|---|---|---|---|
| 更新参数范围 | 全部参数 | 新增的少量参数 | 部分原始参数 |
| 资源消耗 | 非常高 | 极低 | 中等 |
| 存储开销 | 整个模型副本 | 很小(仅需保存增量参数) | 整个模型副本 |
| 灾难性遗忘 | 风险高 | 风险极低 | 风险中等 |
| 性能潜力 | 可能最高 | 通常接近全量微调 | 取决于解冻层数 |
| 易用性 | 简单直接 | 需要选择/配置方法(如LoRA) | 需要选择解冻层 |
现代建议:对于大语言模型的微调,参数高效微调(PEFT),尤其是 LoRA 及其变体,已经成为事实上的标准。它以一种非常巧妙的“增量”方式,在性能、效率和灵活性之间取得了绝佳的平衡。除非你有极其特殊的理由和充足的资源,否则都应优先考虑 PEFT 方法。
二、GPT-2模型介绍
我们现阶段的所有大模型其实都是基于 GPT-2模型发展来的,而这个自然语义 GPT-2 模型是基于2018年的一篇论文(Attention is all you need)揭示的一种新的模型结构,叫做 transformer。
以前自然语义模型用的是 seq2seq,主要用于机器翻译。transformer 模型实际是对自然语义模型 seq2seq 的改进,在这个模型推出来之后,就使得机器翻译就进入了一个全新的时代。
这个 transformer 分为两个部分,第一个部分叫做编码器:它主要是用来做特征提取的;第二个部分叫解码器:主要是根据特征来生成/还原数据的。
这个模型的编码器和解码器可以单独拿出来使用,当时这个编码器有自己的名称:叫 BERT;这个解码器当时的名称叫 GPT。

对于编解码结构来讲,这个 BERT 模型是可以单独拿出来作为一个特征提取器的,可以对输入的文本进行分类(因为特征提取器从本质上来讲就是个判别模型)。
对于编码结构来讲,这个 GPT 单独拿出来用作为一个生成模型,所以说对于一些生成任务来说是一个很好的起点,然后 GPT 紧跟着就推出 GPT-2、GPT-3。目前 GPT-x系列的大模型系列都是从 GPT 这个模型架构发展而来的。
三、Hugging face调用GPT-2模型
在 hugging face上搜索 gpt-chinese 相关模型,主要有中文白话文模型、中文歌词模型、中文古诗词模型、中文对联模型。

-
中文白话文:
gpt2-chinese-cluecorpussmall -
中文诗歌:
gpt2-chinese-lyric -
中文文言文:
gpt2-chinese-ancient -
中文对联:
gpt2-chinese-couplet -
中文诗词:
gpt2-chinese-poem
3.1 中文白话文模型
使用
Hugging face的transformers库调用在线大模型。
# 中文白话文文章生成
from transformers import GPT2LMHeadModel,BertTokenizer,TextGenerationPipeline# 加载模型和分词器
model = GPT2LMHeadModel.from_pretrained(r"...\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3")
tokenizer = BertTokenizer.from_pretrained(r"...\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3")
print(model)# 使用Pipeline调用模型
text_generator = TextGenerationPipeline(model,tokenizer,device="cuda")# 使用text_generator生成文本
# do_sample是否进行随机采样,设置为True时每次生成的结果都不一样;设置为为False时每次生成的结果都是相同的
for i in range(3):print(text_generator("这是很久之前的事情了,", max_length=100, do_sample=True))
输出如下:
GPT2LMHeadModel((transformer): GPT2Model((wte): Embedding(21128, 768)(wpe): Embedding(1024, 768)(drop): Dropout(p=0.1, inplace=False)(h): ModuleList((0-11): 12 x GPT2Block((ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)(attn): GPT2Attention((c_attn): Conv1D(nf=2304, nx=768)(c_proj): Conv1D(nf=768, nx=768)(attn_dropout): Dropout(p=0.1, inplace=False)(resid_dropout): Dropout(p=0.1, inplace=False))(ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)(mlp): GPT2MLP((c_fc): Conv1D(nf=3072, nx=768)(c_proj): Conv1D(nf=768, nx=3072)(act): NewGELUActivation()(dropout): Dropout(p=0.1, inplace=False))))(ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True))(lm_head): Linear(in_features=768, out_features=21128, bias=False)
)[{'generated_text': '这是很久之前的事情了, 了 这 家 店 的 介 绍 也 就 是 这 家 店 的 了. 当 时 它 在 天 津 还 是 第 一 家 开 分 店 的. 后 来 搬 到 天 津 去 了...'}][{'generated_text': '这是很久之前的事情了, 开 了 好 多 年 了 这 家 店 一 直 没 有 开. 现 在 搬 走 了. 但 是 每 次 来 这 里 吃 饭 都 是 要 等 位 的...'}][{'generated_text': '这是很久之前的事情了, 家 常 菜 馆 的 风 格 可 以 说 是 很 不 错 的, 这 是 一 家 很 早 就 开 始 的 了. 店 面 很 小 但 是 很 干 净...'}]
-
GPT-2模型基于transformer构建的,首先是GPT2Model的embedding模型:词向量wte和位置编码wpe。 -
h是隐藏层部分,里面构建了隐藏层ModuleList,包含特征提取的部分。 -
lm_head是生成头,这一层就是用来生成文本的,输出的特征是out_features=21128,与config.json文件中vocab_size一样,恰好等于词表数量。
{"activation_function": "gelu_new","architectures": ["GPT2LMHeadModel"],"attn_pdrop": 0.1,"embd_pdrop": 0.1,"gradient_checkpointing": false,"initializer_range": 0.02,"layer_norm_epsilon": 1e-05,"model_type": "gpt2","n_ctx": 1024,"n_embd": 768,"n_head": 12,"n_inner": null,"n_layer": 12,"n_positions": 1024,"output_past": true,"resid_pdrop": 0.1,"task_specific_params": {"text-generation": {"do_sample": true,"max_length": 320}},"tokenizer_class": "BertTokenizer","vocab_size": 21128
}
核心问题:生成式模型是如何生成文本的?
所有的生成模型:1)都得基于提示词生成;2)都有一个最大生成长度 max_length。
相当于让模型基于提示词完成后面的填空,前面给了提示词,模型会把提示词当成输入,然后模型输出的时候留了 max_length 个空格(当前为100),那空格它咋填呢?当我们把提示词输给模型的时候,得到一个 21128 维的输出,输出的概率代表接下来的那个空格应该填哪个字(字就是从 vocab.txt 字典里面取)。一般的做法是选择 21128 维里面概率最大值所对应的那个字符,这样就得到下一个字符,因为 max_length 值是100,后面还有99个空格需要填写,那么接下来把提示词和输出的第一个字符加起来构成一个新的输入,又得到21128维输出,以此类推…
但是模型如何控制输出长短不一的内容?主要是通过后处理(对话模版)实现的:后处理的逻辑就是检索模型生成的字符,一旦检索到 CLS字符就不再调用大模型继生成了,因为后面是要用 PAD 补齐的部分。
文本在编码的时候有个特殊编码
special_token,例如:SEP和CLS分别代表文本的开头和结束。真实的样本数据有长有短,在做数据集的时候需要补齐数据长度,开头部分用SEP,结尾部分用CLS,然后不够的部分用PAD补齐。那么特殊字符有用呢?在模型生成输出的时候有一个逻辑判断:如果模型生成了CLS特殊字符就不再继续输出了。
3.2 中文歌词生成模型
将模型换成中文歌词生成的模型:
# 中文歌词生成
from transformers import BertTokenizer,GPT2LMHeadModel,TextGenerationPipeline# 加载模型和分词器
model = GPT2LMHeadModel.from_pretrained(r"...\gpt2-chinese-module\models--uer--gpt2-chinese-lyric\snapshots\4a42fd76daab07d9d7ff95c816160cfb7c21684f")
tokenizer = BertTokenizer.from_pretrained(r"...\gpt2-chinese-module\models--uer--gpt2-chinese-lyric\snapshots\4a42fd76daab07d9d7ff95c816160cfb7c21684f")
# print(model)#使用Pipeline调用模型
text_generator = TextGenerationPipeline(model,tokenizer,device="cpu")#使用text_generator生成文本
#do_sample是否进行随机采样。为True时,每次生成的结果都不一样;为False时,每次生成的结果都是相同的。
for i in range(3):print(text_generator("这是很久之前的事情了,", max_length=100, do_sample=True))
输出如下:
[{'generated_text': '这是很久之前的事情了, 那 是 一 个 寒 冷 的 冬 天 , 我 们 去 了 海 边 , 看 到 了 美 丽 的 海 鸥 , 那 是 一 个 美 丽 的 季 节 , 白 色 的 沙 滩 上 , 留 下 了 我 们 的 足 迹 , 你 说 那 是 你 的 故 事 , 我...'}][{'generated_text': '这是很久之前的事情了, 当 初 我 们 并 肩 走 过 , 也 曾 经 快 乐 的 卿 卿 我 我 , 如 今 却 要 面 对 分 离 , 这 是 很 久 之 前 的 事 情 了 , 当 初 我 们 并 肩 走 过 , 也 曾 经 快 乐 的 卿 卿 我 我 , 如 今...'}][{'generated_text': '这是很久之前的事情了, 从 前 的 我 还 是 个 孩 子 子 , 我 还 是 一 个 孩 子 , 我 还 是 一 个 孩 子 , 我 总 是 在 你 面 前 哭 泣 , 你 说 我 们 都 长 大 了 , 我 却 还 是 一 个 孩 子 , 我 总 是 在 你 面 前...'}]
其实本质上来讲,与上面模型没有太大区别,但是生成的内容就会发生一个明显的变化,由白话文风格转化为中文歌词风格,这就是微调或者训练的意义,即跟模型没有关系,主要训练数据决定的。
3.3 中文诗词生成模型
将模型换成中文诗词生成的模型:
# 中文诗词
from transformers import BertTokenizer,GPT2LMHeadModel,TextGenerationPipeline# 加载模型和分词器
model = GPT2LMHeadModel.from_pretrained(r"...\models--uer--gpt2-chinese-poem\snapshots\6335c88ef6a3362dcdf2e988577b7bafeda6052b")
tokenizer = BertTokenizer.from_pretrained(r"...\models--uer--gpt2-chinese-poem\snapshots\6335c88ef6a3362dcdf2e988577b7bafeda6052b")
print(model)#使用Pipeline调用模型
text_generator = TextGenerationPipeline(model,tokenizer,device="cuda")#使用text_generator生成文本
#do_sample是否进行随机采样。为True时,每次生成的结果都不一样;为False时,每次生成的结果都是相同的。
for i in range(3):print(text_generator("白日依山尽,", max_length=50, do_sample=True))
输出如下:

模型的输出有的是4个字,有的是5个字。可知GPT-2模型只能做特征和信息的学习,如果对文本格式有特殊要求需要通过后处理来完成。那么接下来进行自定义数据集训练 gpt-2 模型来实现工整的古诗词的输出。
四、hugging face微调GPT-2模型
通过自定义数据集来微调 GPT-2 模型,为了保证微调是有效的,选择中文白话文模型作为基座模型,我们这要做的就是把白话文生成模型变成古诗词生成模型。
4.1 准备数据
通过爬虫爬取网站上的诗词数据,经过处理后一行是一首诗(不写诗的名称,只写诗的内容)。

4.2 数据处理
接下来对数据集做一个处理。
注意:生成模型跟判别模型的数据集最大的区别在于:生成模型的文本既是
text又是label。
我们还是自己定义一个 MyDataset 类去继承 Dataset,然后重载它里面三个方法:
__init____len____getitem__
代码如下:
from torch.utils.data import Datasetclass MyDataset(Dataset):def __init__(self):with open("data/chinese_poems.txt",encoding="utf-8") as f:lines = f.readlines()lines = [i.strip() for i in lines]self.lines = linesdef __len__(self):return len(self.lines)def __getitem__(self, item):return self.lines[item]if __name__ == '__main__':dataset = MyDataset()for data in dataset:print(data)
BERT 的数据处理中 __getitem__返回 text 和 label ,但是现在数据集只返回了文本。就简单来讲,模型把训练文本分成两段,一段上文一段下文,一般来讲,上文是输入,下文是标签。
我们在进行模型训练的时候,相当于把句子切分成两个部分,先拿第一部分给到模型,之后将模型的输出与剩下的文本做损失计算。
4.3 模型训练
1)环境推荐
做全量微调的时候对设备就有要求了,所以不推荐在自己电脑上跑这个模型,建议使用云服务器(AutoDL) + VSCode(带远程连接Linux和端口转发工具,后续比较方便)。


nvidia-smi 命令查看设备信息:

2)训练模型
接下来我们就开始训练一下模型:
from transformers import AdamW
from transformers.optimization import get_scheduler
import torch
from data import MyDataset # 导入自定义的数据集类
from transformers import AutoModelForCausalLM, AutoTokenizer # 导入transformers的模型和分词器类
from torch.utils.data import DataLoader # 导入PyTorch的数据加载器类# 实例化自定义数据集
dataset = MyDataset() # 创建数据集对象# 加载预训练的分词器,用于文本编码
tokenizer = AutoTokenizer.from_pretrained(r"...\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3")
# 加载预训练的模型,用于语言模型任务
model = AutoModelForCausalLM.from_pretrained(r"...\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3")# 定义一个函数,用于将文本数据转换为模型所需的格式
def collate_fn(data):# 使用分词器对数据进行编码,并填充或截断到固定长度data = tokenizer.batch_encode_plus(data,padding=True, # 填充序列truncation=True, # 截断序列max_length=512, # 最大长度return_tensors='pt') # 返回PyTorch张量# 复制输入ID作为标签,用于语言模型训练data['labels'] = data['input_ids'].clone()return data# 使用DataLoader创建数据加载器,用于批量加载数据
loader = DataLoader(dataset=dataset, # 指定数据集batch_size=2, # 指定批量大小shuffle=True, # 打乱数据drop_last=True, # 如果最后一个批次的数据量小于batch_size,则丢弃collate_fn=collate_fn # 指定如何从数据集中收集样本到批次中
)
print(f"数据的长度:{len(loader)}") # 打印数据加载器中的批次数量# 定义训练函数
def train():# 定义训练参数EPOCH = 3000 # 训练轮数global model # 使用全局模型变量DEVICE = "cuda" if torch.cuda.is_available() else "cpu" # 检测是否有GPU,如果有则使用,否则使用CPUmodel = model.to(DEVICE) # 将模型移动到指定设备# 定义优化器optimizer = AdamW(model.parameters(), lr=2e-5) # 使用AdamW优化器,并设置学习率# 定义学习率调度器scheduler = get_scheduler(name="linear", # 线性调度器num_warmup_steps=0, # 预热步数num_training_steps=len(loader), # 总训练步数optimizer=optimizer)model.train() # 将模型设置为训练模式for epoch in range(EPOCH): # 循环每一轮训练for i, data in enumerate(loader): # 遍历数据加载器中的批次for k in data.keys(): # 将数据移动到指定设备data[k] = data[k].to(DEVICE)out = model(**data) # 前向传播loss = out['loss'] # 获取损失loss.backward() # 反向传播torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪,防止梯度爆炸optimizer.step() # 更新模型参数scheduler.step() # 更新学习率optimizer.zero_grad() # 清空优化器的梯度model.zero_grad() # 清空模型的梯度if i % 50 == 0: # 每隔50个批次打印一次信息labels = data["labels"][:, 1:] # 获取真实标签,忽略<bos>标记out = out["logits"].argmax(dim=2)[:,:-1] # 获取预测结果,忽略<eos>标记select = labels != 0 # 选择非填充的标签labels = labels[select] # 应用选择out = out[select] # 应用选择del select # 删除不再使用的select# 计算准确率acc = (labels == out).sum().item() / labels.numel() # 计算准确率的公式lr = optimizer.state_dict()["param_groups"][0]['lr'] # 获取当前学习率# 打印训练信息print(f"epoch:{epoch},batch:{i},loss:{loss.item()},lr:{lr},acc:{acc}")# 保存最后一轮模型参数torch.save(model.state_dict(), "params/net.pt") # 保存模型参数到指定路径print("权重保存成功!") # 打印成功信息# 当该脚本作为主程序运行时,调用训练函数
if __name__ == '__main__':train() # 开始训练过程
我们现在训练的模型跟上节课有一个很大的区别:当时 BERT 模型用的是增量微调,只训练全连接部分,BERT 模型本身是没参与训练的;今天我们要去训练一个完整的生成模型,所以这种训练叫做全量微调。因此这次训练时给的批次就很小了。
3)观察GPU
nvitop 查看GPU监控信息:
- 如果想查看目前
GPU的一个资源情况,需要安装一个插件:pip install nvitop。装好之后就可以使用nvitop命令去查看目前的设备信息了。主要看显存和GPU核心利用率。

这个批次给这到20的话,目前显存占用 80%,最好让显存占 90% 左右。
4)观察训练指标
任何一个模型,我们在训练的时候先看损失 Loss,前几轮(起码是前1-4轮)后的损失 Loss 整体要呈一个下降趋势,才能证明这个训练是正确的。这个 GPT-2 模型相比前面的模型来讲,因为用的是全量微调,它的训练难度就大太多了。单看损失不会有明显的下降(第1轮的 Loss 基本上在 1.8 左右,第2轮的 Loss 在 1.78 左右,第32轮的损失在1.5左右,看这个波动范围好像 Loss 不怎么降),这个情况是正常的现象。
我们现在只是看 Loss 数据的波动范围,没有图像来的那么直接,如果是图像的话,实际上是可以看到一个下降的趋势,只是这个趋势非常小。
为什么说这个模型的训练是有效呢?注意看另外一个指标:准确率。第一轮的精度整体在 0.2,第二轮的精度接近于 0.3,到32轮的精度到 0.4x;虽然 Loss 从第1轮到32轮没怎么变化,但从精度上面可以看到一个很明显的一个变化,它从最开始 0.2 跑到 0.4x。但是生成模型的精度不能够按照分类模型的精度去评判,分类模型的精度我们可以做到很高(90%+),但是生成模型的精度一般情况在 0.5x 左右就已经很高了。

这个训练我们这才跑了30轮,这对于大模型来说是微乎其微的,一般情况下如果要把一个模型在数据集上面跑的较好的话,至少需要100个轮次才会有比较稳定的表现。
4.4 模型评估
代码如下:
from transformers import AutoModelForCausalLM,AutoTokenizer,TextGenerationPipeline
import torchtokenizer = AutoTokenizer.from_pretrained(r"...\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3")
model = AutoModelForCausalLM.from_pretrained(r"...\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3")# 加载我们自己训练的权重(中文古诗词)
model.load_state_dict(torch.load("params/net.pt"))# 使用系统自带的pipeline工具生成内容
pipeline = TextGenerationPipeline(model,tokenizer,device=0)print(pipeline("天高",max_length =24))
输出结果:

可以看到输出的格式很乱,但可以确定模型学习了诗词的数据集(上述的内容存在诗词数据集中)。
使用后处理进行输出优化,包括:过滤特殊字符、强制添标点符号等。
##### 定制化生成内容 #####
import torch
from transformers import AutoTokenizer,AutoModelForCausalLMtokenizer = AutoTokenizer.from_pretrained(r"...\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3")
model = AutoModelForCausalLM.from_pretrained(r"...\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3")# 加载我们自己训练的权重(中文古诗词)
model.load_state_dict(torch.load("params/net.pt",map_location="cpu"))# 定义函数用于生成5言绝句 text是提示词,row是生成文本的行数,col是每行的字符数。
def generate(text,row,col):# 定义一个内部递归函数,用于生成文本def generate_loop(data):# 禁用梯度计算with torch.no_grad():# 使用data字典中的数据作为模型输入,并获取输出out = model(**data)# 获取最后一个字(logits未归一化的概率输出)out = out["logits"]# 选择每个序列的最后一个logits,对应于下一个词的预测out = out[:,-1]# 找到概率排名前50的值,以此为分界线,小于该值的全部舍去topk_value = torch.topk(out,50).values# 获取每个输出序列中前50个最大的logits(为保持原维度不变,需要对结果增加一个维度,因为索引操作会降维)topk_value = topk_value[:,-1].unsqueeze(dim=1)# 将所有小于第50大的值的logits设置为负无穷,减少低概率词的选择out = out.masked_fill(out < topk_value,-float("inf"))# 将特殊符号的logits值设置为负无穷,防止模型生成这些符号。for i in ",.()《》[]「」{},。":out[:,tokenizer.get_vocab()[i]] = -float('inf')# 去特殊符号out[:, tokenizer.get_vocab()["[UNK]"]] = -float('inf')# 根据概率采样,无放回,避免生成重复的内容out = out.softmax(dim=1)# 从概率分布中进行采样,选择下一个词的IDout = out.multinomial(num_samples=1)# 强值添加标点符号# 计算当前生成的文本长度于预期的长度的比例c = data["input_ids"].shape[1] / (col+1)# 如果当前的长度是预期长度的整数倍,则添加标点符号if c % 1 ==0:if c % 2 ==0:#在偶数位添加句号out[:,0] = tokenizer.get_vocab()["."]else:#在奇数位添加逗号out[:,0] = tokenizer.get_vocab()[","]# 将生成的新词ID添加到输入序列的末尾data["input_ids"] = torch.cat([data["input_ids"],out],dim=1)# 更新注意力掩码,标记所有有效位置data["attention_mask"] = torch.ones_like(data["input_ids"])# 更新token的ID类型,通常在BERTm模型中使用,但是在GPT模型中是不用的data["token_type_ids"] = torch.ones_like(data["input_ids"])# 更新标签,这里将输入ID复制到标签中,在语言生成模型中通常用与预测下一个词data["labels"] = data["input_ids"].clone()# 检查生成的文本长度是否达到或超过指定的行数和列数if data["input_ids"].shape[1] >= row*col + row+1:# 如果达到长度要求,则返回最终的data字典return data# 如果长度未达到要求,递归调用generate_loop函数继续生成文本return generate_loop(data)# 生成3首诗词# 使用tokenizer对输入文本进行编码,并重复3次生成3个样本。data = tokenizer.batch_encode_plus([text] * 3,return_tensors="pt")# 移除编码后的序列中的最后一个token(结束符号)data["input_ids"] = data["input_ids"][:,:-1]# 创建一个与input_ids形状相同的全1张量,用于注意力掩码data["attention_mask"] = torch.ones_like(data["input_ids"])# 创建一个与input_ids形状相同的全0张量,用于token类型IDdata["token_type_ids"] = torch.zeros_like(data["input_ids"])# 复制input_ids到labels,用于模型的目标data['labels'] = data["input_ids"].clone()# 调用generate_loop函数开始生成文本data = generate_loop(data)# 遍历生成的3个样本for i in range(3):#打印输出样本索引和对应的解码后的文本print(i,tokenizer.decode(data["input_ids"][i]))if __name__ == '__main__':generate("白",row=4,col=5)
输出如下:

