0%

大模型应用系列(五) 在云服务器上训练gpt2模型以及通过后处理控制大模型输出

本地连接云服务器,在云服务器上训练模型,通过后处理控制模型的输出。

一. 购买并远程连接云服务器

1.1 租服务器

选在AutoDL平台, 注册后点击算力平台,租对应的服务器,这里我选择按时付费,RTX4090

也可以选择其他平台的服务器。

image-20250314235047571

1.2 远程连接

使用vscode,下载插件Remote-SSH, 复制上面的ssh命令,远程连接服务器。

1.3 训练GPT2

将上一节的代码和模型复制到服务器,模型可以从hf-mirror重新下载,复制时直接复制到vscode的目录下就行(先在vscode打开与远程文件夹),注意修改模型路径。以及根据显存情况修改batchsize, 最好修改到占用90%显存。查看显存可以使用nvitop

使用以下命令下载

1
pip install nvitop

使用以下命令查看显存占用情况

1
nvitop

直接运行train.py文件即可,但这样如果ssh连接中断,训练也会停止,用以下命令可以保证ssh连接中断训练也不停止

1
nohup python tarin.py &

会在目录下生成nohup.out文件用于保存终端输出。

image-20250315000032021

二. 测试训练好的GPT2并进行后处理

本次训练目标是训练一个能生成古诗分格的GPT2。我在服务器上训练了14个epoch,测试部分可以在本地进行,将训练好的参数下载到本地,通过以下代码测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 中文白话文生成
import torch
from transformers import GPT2LMHeadModel, BertTokenizer, TextGenerationPipeline

# 加载模型和分词器
model = GPT2LMHeadModel.from_pretrained('gpt2-chinese-cluecorpussmall')
tokenizer = BertTokenizer.from_pretrained('gpt2-chinese-cluecorpussmall')

# 加载模型参数 map_location将参数和模型放到同个设备上
model.load_state_dict(torch.load('./params/epoch-14', map_location='cpu'))

# 使用Pipeline调用模型
text_generator = TextGenerationPipeline(model, tokenizer, device='cpu') # 如果有cuda,则写cuda

# 生成文本
# max_length控制生成长度, do_sample=True表示进行随机采样,每次生成的结果都不一样,为False时,每次生成的结果都一样
text = text_generator("天高", max_length=100, do_sample=True, truncation=True)
print(text[0]['generated_text'])

输出结果如下,比如我们想输出四句诗,每个句子五个字,共以天高开头个字,(加上标点):

image-20250316232421499

可以看出,和不训练相比(不加载模型参数,输出如下),训练后的模型更接近诗词的形式。

image-20250316232446392

但仍存在一些不足,比如我们原意是想输出每句诗五个字,共四句诗,但输出中有一些句子不是五个字,有一些特殊字符。

我们可以通过后处理使输出更符合我们要的形式。

三. 后处理

大模型只能按照上文推测下一个字符,但它不能严格控制输出格式,所以涉及格式时,我们需要通过后处理进行严格控制。我们需要重新定义生成函数,比如要生成五言绝句,则定义如下函数:

1
def generate(text, row, col):

其中text是提示词, row是生成文本的行数, col是每行的字数,首先这个函数要定义为递归函数,因为我们不知道循环的次数,模型在生成过程中会生成一些不合格的输入,我们会抛弃,所以模型具体生成次数我们不知道。

具体逻辑是在生成过程中获取模型下一个输入的logit,然后根据格式将对应不合法的字符的概率设置为零。从而控制模型的输出。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 通过后处理控制生成格式,使其生成诗词形式。# 中文白话文生成
import torch
from transformers import GPT2LMHeadModel, BertTokenizer, TextGenerationPipeline

# 加载模型和分词器
model = GPT2LMHeadModel.from_pretrained('gpt2-chinese-cluecorpussmall')
tokenizer = BertTokenizer.from_pretrained('gpt2-chinese-cluecorpussmall')

# 加载模型参数 map_location将参数和模型放到同个设备上
model.load_state_dict(torch.load('./params/epoch-14', map_location='cpu'))


# 定义生成函数, 用于生成五言绝句, 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, 即下个词对应的logits
out = out[:, -1]
# 找到概率前50的值, 以此为分界线, 小于该值的全部舍去,然后从top_k中随机采样,这样更有创造性(每次不一样)
topk_value = torch.topk(out, 50).values
# 获取每个输出序列前50个最大的logits,(为保持维度不变,需要对结果增加一个维度,因为索引操作会降维)
topk_value = topk_value[:, -1].unsqueeze(dim=1)
# 将所有logit小于前50个的其他logit设置为负无穷,这样保持了输出形状,在选择的时候也不会选到这些值。
out = out.masked_fill(out < topk_value, -float("inf"))

# 将特殊字符的logits设置为负无穷, 防止模型生成特殊字符
for i in ",.()《》【】{}":
out[:, tokenizer.get_vocab()[i]] = -float('inf')

# 去除特殊符号
out[:, tokenizer.get_vocab()["[SEP]"]] = -float('inf')
out[:, tokenizer.get_vocab()["[UNK]"]] = -float('inf')
out[:, tokenizer.get_vocab()["[CLS]"]] = -float('inf')
# 根据概率采样, 无放回采样, 避免重复生成内容
out = out.softmax(dim=1)

# 从概率分布中进行采样,选择下一个词的ID
out = 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类型,通常在Bert中使用,但GPT不用
data['token_type_ids'] = torch.ones_like(data['input_ids'])

#更新标签,将输入ID复制到标签中,用于预测下个token
data['labels'] = data['input_ids'].clone()

# 检查生成的文本长度是否达到或超过指定的行数和列数
if data['input_ids'].shape[1] >= row * col + row + 1:
# 如果达到长度要求, 返回data
return data

# 如果没达到长度要求, 递归调用
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类型ID
data["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)

运行结果如下:

image-20250317001714886

可以看到, 通过后处理, 三首古诗的格式都复合物五言绝句。

如果您读文章后有收获,可以打赏我喝咖啡哦~