基于 Qwen-0.5B Lora 微调训练英语问答任务
Qwen是阿里巴巴集团的Qwen团队研发的一个大语言模型系列,包含了语言模型和多模态模型。本文是使用了Lora微调的方法对qwen进行微调,使得模型在自己的数据集上更加准确
国庆佳节期间,我总结了一下我的qwen微调过程,前段时间还有一个手搓gpt的微调过程,等待下周我更新一下
1、qwen大语言模型的简介
Qwen是阿里巴巴集团的Qwen团队研发的一个大语言模型系列,包含了语言模型和多模态模型。最新版本的Qwen2是Qwen1.5的重大升级,不管是语言能力还是多模态处理能力,都有了显著提升。Qwen在预训练时使用了海量的多语言和多模态数据,并且通过高质量的数据进行了微调,最终能更贴近人类的偏好。
以下是qwen的网址,具体的可以去网址里面学习一下
https://qwen.readthedocs.io/zh-cn/latest/https://qwen.readthedocs.io/zh-cn/latest/
Qwen2的特点如下所示:
-
易于使用的仅解码器稠密语言模型,提供 0.5B 、1.5B 、3B 、7B 、14B 、32B 和 72B 共7种参数规模的模型,并且有基模型和指令微调模型两种变体(其中“ B ”表示“十亿”, 72B 即为 720 亿)
-
利用我们最新的数据集进行预训练,包含多达 18T tokens (其中“ T ”表示“万亿”, 18T 即为 18 万亿)
-
在遵循指令、生成长文本(超过 8K tokens )、理解结构化数据(例如,表格)以及生成结构化输出特别是 JSON 方面有了显著改进
-
更加适应多样化的系统提示,增强了角色扮演的实现和聊天机器人的背景设置。
-
支持最多达 128K tokens 的上下文长度,并能生成多达 8K tokens 的文本。
-
支持超过 29 种语言,包括中文、英文、法文、西班牙文、葡萄牙文、德文、意大利文、俄文、日文、韩文、越南文、泰文、阿拉伯文等。
总的来说,Qwen2无论是在语言处理还是多模态处理上,都有着非常全面的功能,适合各种复杂场景。
手动下载的步骤如下:
2. 手动下载模型文件
访问模型的页面,并在页面的上找到文件列表,下载以下文件:(这些文件是后来需要用到的,必须下载了)
pytorch_model.bin
:模型的权重文件config.json
:模型的配置文件tokenizer_config.json
:分词器配置文件vocab.json
/tokenizer.json
:分词器词汇表
2、微调步骤的简介
微调一个语言模型,其实就是在一个已经训练过的模型上,继续用新数据进行训练,帮助模型更好地理解和处理这个新的任务。可以把这个过程想象成教一个已经懂很多道理的人去解决新的问题。
这个过程可以分为五个简单的步骤:
1、加载预训练模型和新的数据集:先拿到一个已经训练过的模型,它已经掌握了一些基础能力。然后,再准备一个新的数据集,这个数据包含了我想要这个大模型学会的新任务,比如回答一些英语问题的训练数据。
2、预处理模型和数据集:把数据整理成模型能理解的格式,比如把数学题和答案编码成数字。模型只能理解特定格式的数据,所以需要先做这一步。
3、开始循环训练:训练模型时,它会一遍遍看新数据,慢慢学会解决这些问题。通过训练过程,模型会逐渐调整自己的参数,更好地回答问题。
4、测试模型:一旦模型完成了训练,你可以用它从未见过的测试数据来检查它的表现。测试数据跟训练数据不同,是用来验证模型是否真的学会了这个新任务。
5、评估模型:在测试后,使用一些评价指标来量化模型的表现。比如我们可以通过正确率、准确率等方法来评估它是不是能够很好地解题。
本项目中的所有代码如下:
(具体代码我会整理好上传到我的仓库里,这里先留一个地址的位置)
2.1 Lora简介
LoRA(Low-Rank Adaptation)微调技术的核心思想我认为其实很简单,我觉得可以理解为在外部引入了两个矩阵,我可以在原始的预训练语言模型中添加了一个模块,通常放在Transformer的层内部。这个模块会先做降维操作,再升维回来,这样可以保持模型的输入输出维度不变,同时还能模拟模型的内部低秩结构(intrinsic rank)。通过这样的方式,模型在训练时只需要微调这个模块中的A和B参数,而不需要动到原始模型的大量参数。
具体来说,训练时,原始模型的参数会被冻结,也就是说它们不会被更新。我们只训练旁路中的两个参数矩阵,分别是A和B。当模型进行推理时,旁路的输出会与原始模型的输出叠加,最终影响模型的效果。这种方式可以减少需要训练的参数量,但在性能上却可以媲美甚至超过全参数微调。(非常适合我这种没有什么大量资源的人)
关于A和B参数的初始化,通常的做法是:
- A矩阵:使用随机的高斯分布进行初始化,这样能让模型在训练初期就有一些变化。
- B矩阵:则使用全0矩阵初始化。这样在模型一开始的推理过程中,旁路的输出是0,完全不影响原始模型的表现,模型就能先依赖它原本的能力。
LoRA的优点是,虽然只微调了这个模块AB矩阵中的少量参数,但在大部分任务上,它的效果往往和全参数微调一样好,有时甚至更好,同时大大降低了计算和存储的开销。
2.2 具体微调步骤介绍(重头戏)
就是例如我已经有一个已经学会了日常对话的AI工具人,现在如果想教它解英语的问题,学习一下。首先,我得下载这个AI的这个工具人(模型),然后又要给它准备一套题和答案(新数据集)。然后再把这些题目整理成它能读懂的格式,最后让它一遍又一遍地看和学习这些题目和答案,通过反复练习,逐渐让它变得擅长解题。一旦模型完成了训练,你可以用它从未见过的测试数据来检查它的表现。测试数据跟训练数据不同,是用来验证模型是否真的学会了这个新任务。在测试后,使用一些评价指标来量化模型的表现。比如我们可以通过正确率、准确率等方法来评估它是不是能够很好地解题。
然后比如我要做的这个让他学习问题,然后使用了以下这个数据集:
2.2.1使用的数据集介绍
数据集下载地址:
每条记录包括以下字段:
- id: 问题的唯一标识符。
- answer: 问题的正确答案。
- question: 要回答的问题。
- context: 提供问题答案的上下文段落。
- p_phrase: 正相关短语,用于问题背景中的重要词汇。
- n_phrase: 负相关短语,可能与问题相关但不直接提供答案。
- full answer: 一个完整的句子,描述问题的答案。
包括问题和答案,还提供了上下文段落及相关短语,有助于模型理解背景信息。
2.2.2 qwen大模型的读取(加载已经预训练好的模型)
这里要先下载到本地(由于我的网络不太好用,所以为了避免我需要一直登入那个huggingface社区去找模型,我直接下载到本地了,前面也给了网址,直接下载即可),先拿到一个已经训练过的的大模型,它已经掌握了一些基础能力。然后,再准备一个新的数据集,这个数据包含希望模型学会的新任务,比如我这个英语数据集的训练数据。
图片中是我加载数据集之后的结果,能看见如果我以输入我喜欢小狗为例,这边输入的是什么文本,然后化成了什么token,然后把这个东西输入到模型里面,这样的话就可以输出显示的模型的tensor,(其实正常来说也不用有这一步,我这是为了能更好的展示输入的是什么)
2.2.2 数据处理
本文用的数据集扇面已经介绍过了,这个数据集非常实用,也比较的小,可以在我的本地运行,也是非常好理解的数据集
然后是预处理模型和数据集:把数据整理成模型能理解的格式,这个数据集是json的形式。模型只能理解特定格式的数据,所以需要先做这一步。可以看一下模型的数据集长什么样子,然后根据那个数据集修改之后匹配成我要输入到大模型的格式:
(具体代码我会整理好上传到我的仓库里,这里先留一个地址的位置)
然后,已经看到数据的样子了,我得通过写一个数据的类来控制我这个数据输入的像我那个大模型需要的数据:包括了以下的一些方法:
__init__ 方法:
用于初始化数据集。通过传入数据文件路径和分词器(tokenizer),以及问题和答案的最大长度,加载并存储数据。数据以JSON格式存储,从文件中读取到self.data中。
preprocess 方法:
负责将问题和答案进行编码,将自然语言转化为模型可以接受的输入形式。这里先定义了一个系统消息和用户消息,使用分词器生成问题和答案的input_ids和attention_mask,并对问题的部分标签设置为-100,使其在计算损失时被忽略。
__getitem__ 方法:
负责获取指定索引处的数据项,并对其进行预处理。返回经过填充的input_ids、attention_mask和labels。
pad_tensor 方法:
用于对张量进行填充,使得所有的输入序列长度一致,以方便后续的批处理操作。
__len__ 方法:
返回数据集的大小,即数据条目的数量,用于数据加载器的迭代。
这里直接上代码把,我已经将一些关键点的注释补充进了代码里
class QADataset(Dataset):
def __init__(self, data_path, tokenizer, max_source_length, max_target_length) -> None:
super().__init__()
self.tokenizer = tokenizer # 初始化时传入的分词器,用于对数据进行编码
self.max_source_length = max_source_length # 问题的最大长度
self.max_target_length = max_target_length # 答案的最大长度
self.max_seq_length = self.max_source_length + self.max_target_length # 输入序列的最大长度(问题+答案的长度)
self.data = [] # 存储从文件中加载的数据
if data_path:
# 打开并读取JSON文件,存储到 self.data 中
with open(data_path, "r", encoding='utf-8') as f:
self.data = json.load(f) # 直接读取JSON文件
print("data load, size:", len(self.data)) # 输出加载的数据大小
def preprocess(self, question, answer):
"""
预处理函数:将问题和答案进行处理,并返回编码后的input_ids、attention_mask和labels。
"""
# 构建消息,角色分别为系统(system)和用户(user),系统提供上下文,用户提供问题
messages = [
{"role": "system", "content": "你是一个英语问答系统,可以根据用户的问题进行解答。"},
{"role": "user", "content": question}
]
# 使用分词器生成提示模板,不进行tokenize,添加生成的提示
prompt = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# 使用分词器对prompt进行编码,max_length限制为最大问题长度,不添加特殊符号
instruction = self.tokenizer(prompt, add_special_tokens=False, max_length=self.max_source_length)
# 对答案进行编码
response = self.tokenizer(answer, add_special_tokens=False, max_length=self.max_target_length)
# 将问题和答案编码后的 input_ids 和 attention_mask 组合,并添加填充符号
input_ids = instruction["input_ids"] + response["input_ids"] + [self.tokenizer.pad_token_id]
attention_mask = (instruction["attention_mask"] + response["attention_mask"] + [1])
# labels 只对答案部分有效,问题部分的label标记为 -100 以忽略不计算损失
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [self.tokenizer.pad_token_id]
# 如果序列长度超过最大长度,则进行截断处理
if len(input_ids) > self.max_seq_length:
input_ids = input_ids[:self.max_seq_length]
attention_mask = attention_mask[:self.max_seq_length]
labels = labels[:self.max_seq_length]
return input_ids, attention_mask, labels
def __getitem__(self, index):
"""
获取数据集中的第index条数据,并对问题和答案进行预处理。
"""
item_data = self.data[index] # 获取指定索引的数据条目
# 从数据条目中获取问题和答案
question = item_data['question']
answer = item_data['answer']
# 对问题和答案进行预处理,返回input_ids, attention_mask和labels
input_ids, attention_mask, labels = self.preprocess(question, answer)
# 将输入序列转换为张量
input_ids = torch.LongTensor(input_ids)
attention_mask = torch.LongTensor(attention_mask)
labels = torch.LongTensor(labels)
# 对input_ids, attention_mask和labels进行填充,使得它们的长度一致
input_ids = self.pad_tensor(input_ids)
attention_mask = self.pad_tensor(attention_mask)
labels = self.pad_tensor(labels)
return {
"input_ids": input_ids, # 输入的ID序列
"attention_mask": attention_mask, # 注意力掩码
"labels": labels # 标签,用于训练时的目标序列
}
def pad_tensor(self, tensor):
"""
对张量进行填充,确保其长度符合最大序列长度要求。
"""
padding_length = self.max_seq_length - len(tensor) # 计算需要填充的长度
if padding_length > 0:
# 使用pad_token_id进行填充
padding = torch.full((padding_length,), self.tokenizer.pad_token_id, dtype=torch.long)
tensor = torch.cat([tensor, padding]) # 将填充后的部分拼接到原始张量
return tensor
def __len__(self):
"""
返回数据集的大小,即数据条目的数量。
"""
return len(self.data)
2.2.3 模型微调的结构介绍
我先写了一个代码,让我自己也能更清楚地看到模型的结构,结构如下:
选择模型中合适的线性层作为目标模块。根据初始模型的结构,以下是可能适合应用 LoRA 的模块:
q_proj
: 这个是注意力机制中的查询(Query)投影层,可以作为 LoRA 的目标。k_proj
: 注意力机制中的键(Key)投影层,也可以作为 LoRA 的目标。v_proj
: 注意力机制中的值(Value)投影层。o_proj
: 注意力输出投影层。gate_proj
,up_proj
,down_proj
: 这些是多层感知机(MLP)模块中的投影层,也可以加入 LoRA。
然后,输入了lora的模块之后模型结构如下图所示:
微调前:打印的模型结构是标准 AutoModelForCausalLM 的结构,没有 LoRA 模块,所有层的参数都被列出。
微调后:在应用 LoRA 之后,可以看到特定的层(例如 q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj)中引入了新的 LoRA 模块。这些新增的模块会导致模型中一部分参数变为可训练(只有 LoRA 相关的部分被训练,而非整个模型)。
2.2.4 训练过程+验证过程
训练过程就是直接设置一些重要的参数,比如轮数等,然后直接训练就可以了,这是我对数据集输入的设置:(其他的直接在代码里都有)
剩下的直接上代码就行,我这里是设置了保存每5轮的结果和最好的结果,评价指标设置的f1值,我这边举个例子,比如这里是训练两轮的结果:
2.2.5 推理过程
这里就比较简单了,模型训练完拿出来使用就可以咯
更多推荐
所有评论(0)