目录

一.引言

二.Lora 模型文本生成

1.模型读取

1.1 AutoModelForCausalLM.from_pretrained

1.2  PeftModel.from_pretrained

2.文本生成

2.1 Tokenizer

2.2 model.generate

3.输出实践

三.总结


一.引言

前面介绍了使用 Baichuan7B 从样本生成到 Lora 模型微调和存储的流程,本文介绍基于 Lora 得到的微调模型进行文本生成,从而实现 QA 样本到 Lora 微调到预测的全链路。由于是学习途中,所以本文也会尽可能解释每一个 API 每一个参数,有问题欢迎大家指出交流~

二.Lora 模型文本生成

1.模型读取

为了在预测时体验 Lora 前后模型效果的变化,所以我们同时加载原始 Baichuan7B 和微调后的 Baichuan7B。

from peft import PeftModel
from transformers import AutoTokenizer, AutoModel
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import contextlib
import time
autocast = contextlib.nullcontext

# 加载原始 LLM
model_path = "/model/baichuan-7B"

load_st = time.time()
model = AutoModelForCausalLM.from_pretrained(
model_path, load_in_8bit=False, trust_remote_code=True,
device_map="auto" # 模型不同层会被自动分配到不同GPU上进行计算)
)
load_end = time.time()
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
# 原始 LLM 安装上 Lora 模型
lora_model = PeftModel.from_pretrained(model, "weights/simple_lora_by_baichuan-7B").half()
load_lora_end = time.time()
print("Load Ori Model: %s Load Lora Model: %s" % (cost(load_st, load_end), cost(load_end, load_lora_end)))

下面简单了解下加载原始模型和加载 Lora 模型的 API。

1.1 AutoModelForCausalLM.from_pretrained

AutoModelForCausalLM.from_pretrained 方法是用于从预训练模型加载一个自回归语言模型的快捷方式,这里我们给定了四个参数:

• model_path - 模型地址,原生的读取 Baichuan 即可,Lora 读取训练得到的 weights 下的地址

• load_in_8bit - 设置为 True 时,预训练模型的权重参数会以更低的精度 [8位] 进行存储,从而减少了模型所需的内存空间。

• trust_remote_code - 该参数指示系统在执行远程或外部代码时如何处理安全性和信任性。

• device_map - 在分布式计算环境中,可以使用 "device_map" 参数将不同的任务分配给不同的计算节点或处理器,并利用并行计算来加速整体计算过程。这里使用 auto 自定选择。

除此之外,官方还内置了很多常用的 LM,我们可以通过关键字直接下载加载:

from transformers import AutoModelForCausalLM

# 从预训练模型加载自回归语言模型
model = AutoModelForCausalLM.from_pretrained(
    "gpt2",
    cache_dir="./cache",
    from_tf=False,
    force_download=False,
    resume_download=True,
    use_auth_token=False
)

• pretrained_model_name_or_path - 指定要加载的预训练模型的名称或路径。

• config - 可选参数,可以传递一个预训练模型的配置对象(如GPTConfig)。如果没有提供,将自动从pretrained_model_name_or_path中加载。

• cache_dir - 可选参数,用于指定缓存目录地址。

• from_tf - 可选参数,如果设置为True,则从TensorFlow模型转换而来。

• force_download - 可选参数,如果设置为True,则强制从模型Hub下载模型。

• resume_download - 可选参数,如果设置为True,则在下载过程中断后继续下载

• user_auth_token - 可选参数,如果设置为True,并且你的Hugging Face账户已经配置了token,那么将使用token进行认证。 

1.2  PeftModel.from_pretrained

   @classmethod
    def from_pretrained(
        cls,
        model: PreTrainedModel,
        model_id: Union[str, os.PathLike],
        adapter_name: str = "default",
        is_trainable: bool = False,
        config: Optional[PeftConfig] = None,
        **kwargs: Any,
    ):

• model - 初始化模型,通过 ~transformers.PreTrainedModel.from_pretrained 加载而来

• model_id - 在 HF 托管的 Lora 模型 id 或者 Lora 训练后通过 save_pretrained 保存的路径地址

daapter_name - 要加载的适配器的名称。这对于加载多个适配器非常有用。

• is_trainable - 适配器是否应可训练。如果为“False”,适配器将被冻结并用于推理。

• config - 要使用的配置对象,而不是自动加载的配置。此配置对象与“model_id”和“kwargs”互斥。一般在调用“from_pretrained”之前加载。

Tips:

config 即 peft.PeftConfig 上文 Lora 训练时我们已经介绍了其中的参数,最主要的是低秩 rank r。

peft_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM, # 因果语言模型可以被看作是自回归语言模型的一种扩展,在每个时间步上,它不仅考虑之前的文本序列,还>考虑了未来可能出现的单词信息,从而更好地捕捉语句的因果结构。
        inference_mode=False,
        r=finetune_args.lora_rank,
        lora_alpha=32,
        lora_dropout=0.1,
        target_modules = ["W_pack"] # 把model打印出来,找跟attention相关的模块
    )
 
# 获取 Lora 微调模型
model = get_peft_model(model, peft_config)

 推理阶段我们一般直接加载 Base 模型和 Lora 参数即可,Lora 微调时需要设置 peft.PeftConfig。

2.文本生成

while True:
    inputText = input("请输入信息 [输入'q'退出]\n")
    if inputText == 'q':
        print("Exit!")
        break
    else:
        time_st = time.time()
        inputs = tokenizer(inputText + "\n", return_tensors='pt')
        inputs = inputs.to('cuda:0')
        time_token = time.time()
        ori_pred = model.generate(**inputs, max_new_tokens=512, do_sample=True)
        ori_answer = tokenizer.decode(ori_pred.cpu()[0], skip_special_tokens=True)
        time_ori = time.time()
        lora_pred = lora_model.generate(**inputs, max_new_tokens=512, do_sample=True)
        lora_answer = tokenizer.decode(lora_pred.cpu()[0], skip_special_tokens=True)
        time_lora = time.time()
        print("原始输出:")
        print(ori_answer)
        print('Lora输出:')
        print(lora_answer)
        print("Total Cost: %s Token Cost: %s Ori Cost: %s Lora Cost: %s" % (cost(time_st, time_lora), cost(time_st, time_token), cost(time_token, time_ori), cost(time_ori, time_lora)))

为了可以单次加载多次使用,这里采用 While True 的形式,主要使用 tokenizer 对原始输入进行 token 编码,其次使用 model.generate 进行文本生成,最后用 tokenizer.encode 变成我们可以看懂的语言。为了对比前后差距,我们采用原 Baichuan7B 和 Lora 后的 Baichuan7B 进行人工对比。这里简单了解诶下 tokenizer 和 generate 的参数。

2.1 Tokenizer

tokenizer return_tensors='pt' 代表返回 PyTorch 类的张量。我们可以从 tokenizer 后的 Inputs 获取 input_ids 和 attention_mask 作为 PyTorch 向量。

input="你好,今天天气不错!"
print(tokenizer(input + "\n", return_tensors='pt'))
=>
{'input_ids': tensor([[ 9875, 31213,    72,  3482,  6971,  6891,    80,     5]]), 
 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])}

2.2 model.generate

在 model.generate 方法中,我们主要使用了 max_new_token 和 do_sample 参数

• max_new_token

前者从字面意思就可以看出其含义指定了生成的文本中允许的最大新标记数目。文本会一直生成直到满足下面条件之一: 达到 max_length 限制或生成的文本中新增的 token 数目超过了 max_new_token。该参数和 max_length 主要用于控制生成文本的长度。

• do_sample

该参数决定了是否采用采样策略生成文本。当设置为 True 时,模型将根据概率分布随机选择下一个标记,当设置为 False 时,模型将选择概率最高的下一个标记。通过设置 do_sample = True,我们可以增加生成文本的多样性。

下面给出一个常用的文本生成示例,其中给出了不同参数的含义:

# 使用generate方法生成文本
output = model.generate(
    input_ids=input_ids,  # 输入的token张量
    max_length=100,       # 生成文本的最大长度
    num_return_sequences=5,  # 返回多个生成序列
    max_new_tokens=20,    # 允许的最大新标记数目
    do_sample=True,       # 使用采样策略
)

# 处理生成的输出
for generated_sequence in output:
    generated_text = tokenizer.decode(generated_sequence, skip_special_tokens=True)
    print(generated_text)

Tips:

上述 While True 函数虽然是持续输出,但是每次输出是单调独立的,每一次输出不会考虑过去的输入,即没有记忆性,不考虑 History。

3.输出实践

• 原始样本

{"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
{"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
{"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a":"鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每只
小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}
{"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
{"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
{"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a": "鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每>只小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}
{"q": "请计算:39 * 0 = 什么?", "a": "这是简单的乘法运算,39乘以0得到的是0"}
{"q": "题目:51/186的答案是什么?", "a": "这是简单的除法运算,51除以186大概为0.274"}
{"q": "鹿妈妈买了24个苹果,她想平均分给她的3只小鹿吃,每只小鹿可以分到几个苹果?", "a": "鹿妈妈买了24个苹果,平均分给3只小鹿吃,那么每>只小鹿可以分到的苹果数就是总苹果数除以小鹿的只数。\n24÷3=8\n每只小鹿可以分到8个苹果。所以,答案是每只小鹿可以分到8个苹果。"}

• 输出示例

LLM - 读取 Lora 模型进行文本生成-LMLPHP

我们的训练样本简单的复制了几次 demo 样本,可以看到模型在经过 lora 微调后,可以大致描述清楚,但是存在重复输出的问题。

三.总结

结合前面的样本生成和 Lora 训练,这里我们实现了样本-训练-生成的全链路。有兴趣的同学可以简单尝试该 demo。这一版从样本和训练整体来说都比较简单,后续有时间会更新下带 History 样本的模型微调示例。

QA 数据准备与 DataSet 构建

Lora 微调预训练模型

训练数据构造解析

Lora 微调遇到的问题

07-15 14:50