发布时间:2025-03-20 10:16:25编辑:123阅读(678)
通过上一章平台微调大模型,已经了解了模型微调需要的大部分基础概念,也通过硅基流动平台走完了一个完整的微调流程,在这个过程中发现有几个问题:
可以选择的基础模型太少了,没有想要的 DeepSeek 相关模型。
模型训练过程中的 Token 消耗是要自己花钱的,对于有海量数据集的任务可能消耗比较大。
微调任务触发不太可控,在测试的时候创建的微调任务,等了很久还没有被触发,可能是硅基流动最近调用量太大,资源不足的问题,但是总归这个任务还是不太可控的。
为了解决这个问题,最终我们还是要使用代码来微调,这样我们就能灵活选择各种开源模型,无需担心训练过程中的 Token 损耗,灵活的控制微调任务了
进阶:要了解的工具
后续模型微调过程中需要用到的两个核心工具Colab 和 Unsloth。
Colab 是一个基于云端的编程环境,由 Google 提供。它的主要功能和优势包括:免费的 GPU 资源:Colab 提供免费的 GPU,适合进行模型微调。虽然免费资源有一定时间限制,但对于大多数微调任务来说已经足够。易于上手:Colab 提供了一个基于网页的 Jupyter Notebook 环境,用户无需安装任何软件,直接在浏览器中操作。丰富的社区支持:Colab 上有许多现成的代码示例和教程,可以帮助新手快速入门。
简单来说,有了 Colab ,可以让你没有在比较好的硬件资源的情况下,能够在线上微调模型,如果只是学习的话,免费的资源就够了。另外,市面上很多模型微调的 DEMO ,都是通过 Colab 给出的,大家可以非常方便的直接进行调试运行。
Unsloth 是一个开源工具,专门用来加速大语言模型(LLMs)的微调过程。它的主要功能和优势包括:高效微调:Unsloth 的微调速度比传统方法快 2-5 倍,内存占用减少 50%-80%。这意味着你可以用更少的资源完成微调任务。低显存需求:即使是消费级 GPU(如 RTX 3090),也能轻松运行 Unsloth。例如,仅需 7GB 显存就可以训练 1.5B 参数的模型。支持多种模型和量化:Unsloth 支持 Llama、Mistral、Phi、Gemma 等主流模型,并且通过动态 4-bit 量化技术,显著降低显存占用,同时几乎不损失模型精度。开源与免费:Unsloth 提供免费的 Colab Notebook,用户只需添加数据集并运行代码即可完成微调。
简单来说,Unsloth 采用了某些优化技术,可以帮助我们在比较低级的硬件设备资源下更高效的微调模型。在 Unsloth 出现之前,模型微调的成本非常高,普通人根本就别想了,微调一次模型至少需要几万元,几天的时间才能完成。我们看到 Unsloth 官方提供了很多通过 Colab 提供的各种模型的微调案例,我们可以很方便的在 Colab 上直接运行这些案例。
实战:使用unsloth微调模型
安装unsloth前:
建议的思路为先查看NVIDIA显卡驱动版本号,之后确定要安装的Pytorch版本,根据Pytorch官网确定CUDA版本,根据CUDA版本确定cuDNN版本。
如果上述过程中有某一项确定不了版本号,建议选择低一个版本号的Pytorch版本再次确定CUDA版本和cuDNN版本。
需要理清几个概念:
1 CUDA
CUDA(ComputeUnified Device Architecture),是显卡厂商NVIDIA推出的通用并行计算平台和编程模型,
可以帮助开发者充分利用GPU的并行计算能力,该架构使GPU能够解决复杂的计算问题。
2 cuDNN
cuDNN全称NVIDIA CUDA Deep Neural Network library,是一个用于深度神经网络的GPU加速库。
它强调性能、易用性和低内存开销。cuDNN包含了为神经网络中常见的计算任务提供高度优化的实现,
包括前向卷积、反向卷积、注意力机制、矩阵乘法(matmul)、池化(pooling)和归一化(normalization)等。
cuDNN是基于CUDA的深度学习GPU加速库,有了它才能在GPU上完成深度学习的计算。
它就相当于工作的工具,比如它就是个扳手。但是CUDA这个工作台买来的时候,并没有送扳手。
想要在CUDA上运行深度神经网络,就要安装cuDNN,就像你想要拧个螺帽就要把扳手买回来。
3 CUDA和cuDNN的关系
CUDA看作是一个工作台,上面配有很多工具,如锤子、螺丝刀等。cuDNN是基于CUDA的深度学习GPU加速库,
有了它才能在GPU上完成深度学习的计算。它就相当于工作的工具,比如它就是个扳手。
但是CUDA这个工作台买来的时候,并没有送扳手。想要在CUDA上运行深度神经网络,就要安装cuDNN,
就像你想要拧个螺帽就要把扳手买回来。这样才能使GPU进行深度神经网络的工作,工作速度相较CPU快很多。
简单来说,CUDA提供了并行运算的基础设施,而cuDNN在此基础上提供了深度学习所需的特定功能。
如何使用 WSL 在 Windows 上安装 Linux
文档地址:https://learn.microsoft.com/zh-cn/windows/wsl/install
开发人员可以在 Windows 计算机上同时访问 Windows 和 Linux 的强大功能。 通过适用于 Linux 的 Windows 子系统 (WSL),开发人员可以安装 Linux 发行版(例如 Ubuntu、OpenSUSE、Kali、Debian、Arch Linux 等),并直接在 Windows 上使用 Linux 应用程序、实用程序和 Bash 命令行工具,不用进行任何修改,也无需承担传统虚拟机或双启动设置的费用.
点击搜索,搜索 windows功能,双击
打勾 : 适用于Linux的windows子系统
确定后需要重启电脑。如果没有这个选项的,需要在BIOS里面设置开启虚拟化支持。
重启后,cmd以管理员身份运行
命令 wsl --help
显示上面则安装成功。
安装Linux系统,这里选择安装的是Ubuntu系统,也可以选其它版本的linux系统。
命令:wsl --install -d Ubuntu
启动Ubuntu系统
命令 wsl -d ubuntu
点击搜索ubuntu,找到文件所在位置,发送桌面快捷图标
直接双击打开ubuntu
更新Ubuntu本地软件包列表
sudo apt update
安装基本编译工具
sudo apt install build-essential -y
sudo apt install cmake -y
CUDA下载
cd /opt
sudo wget https://developer.download.nvidia.com/compute/cuda/12.1.0/local_installers/cuda_12.1.0_530.30.02_linux.run
安装CUDA
sudo sh cuda_12.1.0_530.30.02_linux.run
安装Anaconda
sudo wget https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-x86_64.sh
sudo bash Anaconda3-2024.10-1-Linux-x86_64.sh
一路yes安装完,重新进去ubuntu,前面会有个base
使用conda创建虚拟python环境
conda create --name my_unsloth_env python=3.11
显示下面代表成功
激活虚拟环境,可以看到前面变成了虚拟项目名了
conda activate my_unsloth_env
安装pytorch
conda install pytorch==2.4.0 torchvision==0.19.0 torchaudio==2.4.0 pytorch-cuda=12.1 -c pytorch -c nvidia
显示done表示完成
在python中确认一下torch是否安装成功。
python
Python 3.11.11 (main, Dec 11 2024, 16:28:39) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> print(torch.cuda.device_count())
1
>>> print(torch.cuda.is_available())
True
>>> print(torch.__version__)
2.4.0
>>> print(torch.version.cuda)
12.1
>>>
unsloth安装,官方文档:https://github.com/unslothai/unsloth
pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git" -i https://mirrors.aliyun.com/pypi/simple
加载预训练模型
看到这里的参数是 model_name,然后选择的是 DeepSeek-R1-Distill-Llama-8B(基于 Llama 的 DeepSeek-R1 蒸馏版本,80 亿参数),然后运行代码我们可以看到模型的拉取日志:
# 导入FastLanguageModel类,用来加载和使用模型 from unsloth import FastLanguageModel # 导入torch工具 ,用于处理模型的数学运算 import torch # 设置模型处理文本的最大长度,相当于给模型设置一个最大容量 max_seq_len = 2048 # 设置数据类型,让模型自动选择最合适的精度 dtype = None # 使用4位量化来节省内存,就像把大箱子压缩成小箱子 load_in_4bit = True model, tokenizer = FastLanguageModel.from_pretrained( model_name = "unsloth/DeepSeek-R1-Distill-Llama-8B", # 指定要加载的模型名称 max_seq_length = max_seq_len, # 使用上面定义的最大长度 dtype = dtype, # 使用前面设置的数据类型 load_in_4bit = load_in_4bit, # 使用4位量化 )
在当前目录新建一个text.py文件,把上面的代码复制进去执行(需要梯子,会在huggingface上下载模型)
python text.py
下载完成。
微调前测试,先用一个算命相关的问题来测试一下,方便我们在训练完后进行对比。
在/opt目录下创建一个before_fine_tuning_test.py文件
python代码如下:
from unsloth import FastLanguageModel # 设置模型处理文本的最大长度,相当于给模型设置一个最大容量 max_seq_len = 2048 # 设置数据类型,让模型自动选择最合适的精度 dtype = None # 使用4位量化来节省内存,就像把大箱子压缩成小箱子 load_in_4bit = True model, tokenizer = FastLanguageModel.from_pretrained( model_name = "unsloth/DeepSeek-R1-Distill-Llama-8B", # 指定要加载的模型名称 max_seq_length = max_seq_len, # 使用上面定义的最大长度 dtype = dtype, # 使用前面设置的数据类型 load_in_4bit = load_in_4bit, # 使用4位量化 ) prompt_style = """ 以下是描述任务的指令,以及提供进一步上下文的输入。 请写出一个适当完成请求的回答。 在回答之前,请仔细思考,并创建一个逻辑连贯的思考过程,以确保回答准确无误。 ### 指令: 你是一位精通占卜、星象和运势预测的算命大师。 请回答以下算命问题。 ### 问题: {} ### 回答: <think>{}""" # 定义提示风格的字符串模板,用于格式化问题 question = "1992年闰四月初九巳时生人,女,想了解健康运势" # 定义具体的算命问题 FastLanguageModel.for_inference(model) # 准备模型以进行推理 inputs = tokenizer([prompt_style.format(question, "")], return_tensors="pt").to("cuda") # 使用 tokenizer 对格式化后的问题进行编码,并移动到GPU outputs = model.generate(input_ids=inputs.input_ids, attention_mask=inputs.attention_mask, max_new_tokens=1200, use_cache=True) # 使用模型生成回答 response = tokenizer.batch_decode(outputs) # 解码模型生成的输出为可读文本 print(response[0]) # 打印生成的回答部分
结果如下:
没有给出答案。
加载数据集(数据清洗)
首先把这个数据集预期要训练出来的模型风格定义出来。
下面要准备一个用于微调的数据集,可以去HuggingFace上搜索自己需要的数据集。
要注意的是,这里字段格式和前面提到的格式略有区别,除了包含基本的问题(Question)、回答(Response),还包含了模型的思考过程(Complex_CoT),因为现在要训练的是一个推理模型,所以数据集中最好也要包含模型的思考过程,这样训练出来的推理模型效果更好。下面看看加载数据集的代码,把数据集加载进来,然后打印出数据集包含的字段名,在/opt目录下新建data_cleaning.py文件,完整代码如下:
from unsloth import FastLanguageModel # 设置模型处理文本的最大长度,相当于给模型设置一个最大容量 max_seq_len = 2048 # 设置数据类型,让模型自动选择最合适的精度 dtype = None # 使用4位量化来节省内存,就像把大箱子压缩成小箱子 load_in_4bit = True model, tokenizer = FastLanguageModel.from_pretrained( model_name = "unsloth/DeepSeek-R1-Distill-Llama-8B", # 指定要加载的模型名称 max_seq_length = max_seq_len, # 使用上面定义的最大长度 dtype = dtype, # 使用前面设置的数据类型 load_in_4bit = load_in_4bit, # 使用4位量化 ) train_prompt_style = """ 以下是描述任务的指令,以及提供进一步上下文的输入。 请写出一个适当完成请求的回答。 在回答之前,请仔细思考,并创建一个逻辑连贯的思考过程,以确保回答准确无误。 ### 指令: 你是一位精通八字算命、紫微斗数、风水、易经卦象、塔罗牌占卜、星象、面相手相和运势预测等方面的算命大师。 请回答以下算命问题。 ### 问题: {} ### 回答: <思考> {} </思考> {}""" # 定义结束标记(EOS_TOKEN),用于指示文本的结束 EOS_TOKEN = tokenizer.eos_token # 必须添加结束标记 # 导入数据集加载函数 from datasets import load_dataset # 加载指定的数据集,选择中文语言和训练集的前500条记录 dataset = load_dataset('Conard/fortune-telling', 'default', split='train[0:200]', trust_remote_code=True) # 打印数据集的列名,查看数据集中有那些字段 print(dataset.column_names) # 定义一个函数,用于格式化数据集中的每条记录 def formatting_prompt_func(examples): # 从数据集中提取问题,复杂思考过程和回答 inputs = examples['Question'] cots = examples['Complex_CoT'] outputs = examples['Response'] texts = [] # 用于存储格式化后的文本 # 遍历每个问题,思考过程和回答,进行格式化 for input, cot, output in zip(inputs, cots, outputs): # 使用字符串模板插入数据,并加上结束标记 text = train_prompt_style.format(input, cot, output) + EOS_TOKEN texts.append(text) # 将格式化后的文本添加到列表中 return { "text": texts, # 返回包含所有格式化文本的字典 } dataset = dataset.map(formatting_prompt_func, batched=True) ret = dataset['text'][0] print(ret)
结果如下:
目前所有准备工作已经完成,下一步就是开始微调训练。
使用Unsloth执行微调
在这一步,我们需要设置各种关键参数,把关键代码分为三段。
第一段(模型微调准备):
这段代码是通过 LoRA 技术对预训练模型进行了微调准备,使其能够在特定任务上进行高效的训练,同时保留预训练模型的大部分知识。LoRA 在这篇文章中不做深度讲解,先了解即可,参数也先不用改。
第二段(配置微调参数)
在这段代码中,包括一大堆参数,不需要都理解,只需要关注上面已经介绍过的三个参数:学习率(Learning Rate):通过 TrainingArguments 中的 learning_rate 参数设置的,这里的值为 2e-4(即 0.0002)。批量大小(Batch Size):由两个参数共同决定(实际的批量大小:per_device_train_batch_size * gradient_accumulation_steps,也就是 2 * 4 = 8):per_device_train_batch_size:每个设备(如 GPU)上的批量大小。gradient_accumulation_steps:梯度累积步数,用于模拟更大的批量大小。训练轮数(Epochs):通过 max_steps(最大训练步数) 和数据集大小计算得出,在这段代码中,最大训练 70 步,每一步训练 8 个,数据集大小为 200,那训练论数就是 70 * 8 / 200 = 3
第三段模型训练
trainer_stats = trainer.train()
完整代码如下
from unsloth import FastLanguageModel # 设置模型处理文本的最大长度,相当于给模型设置一个最大容量 max_seq_len = 2048 # 设置数据类型,让模型自动选择最合适的精度 dtype = None # 使用4位量化来节省内存,就像把大箱子压缩成小箱子 load_in_4bit = True model, tokenizer = FastLanguageModel.from_pretrained( model_name = "unsloth/DeepSeek-R1-Distill-Llama-8B", # 指定要加载的模型名称 max_seq_length = max_seq_len, # 使用上面定义的最大长度 dtype = dtype, # 使用前面设置的数据类型 load_in_4bit = load_in_4bit, # 使用4位量化 ) prompt_style = """ 以下是描述任务的指令,以及提供进一步上下文的输入。 请写出一个适当完成请求的回答。 在回答之前,请仔细思考,并创建一个逻辑连贯的思考过程,以确保回答准确无误。 ### 指令: 你是一位精通占卜、星象和运势预测的算命大师。 请回答以下算命问题。 ### 问题: {} ### 回答: <think>{}""" # 定义提示风格的字符串模板,用于格式化问题 question = "1992年闰四月初九巳时生人,女,想了解健康运势" # 定义具体的算命问题 FastLanguageModel.for_inference(model) # 准备模型以进行推理 inputs = tokenizer([prompt_style.format(question, "")], return_tensors="pt").to("cuda") # 使用 tokenizer 对格式化后的问题进行编码,并移动到GPU outputs = model.generate(input_ids=inputs.input_ids, attention_mask=inputs.attention_mask, max_new_tokens=1200, use_cache=True) # 使用模型生成回答 response = tokenizer.batch_decode(outputs) # 解码模型生成的输出为可读文本 print(response[0]) # 打印生成的回答部分 train_prompt_style = """ 以下是描述任务的指令,以及提供进一步上下文的输入。 请写出一个适当完成请求的回答。 在回答之前,请仔细思考,并创建一个逻辑连贯的思考过程,以确保回答准确无误。 ### 指令: 你是一位精通八字算命、紫微斗数、风水、易经卦象、塔罗牌占卜、星象、面相手相和运势预测等方面的算命大师。 请回答以下算命问题。 ### 问题: {} ### 回答: <思考> {} </思考> {}""" # 定义结束标记(EOS_TOKEN),用于指示文本的结束 EOS_TOKEN = tokenizer.eos_token # 必须添加结束标记 # 导入数据集加载函数 from datasets import load_dataset # 加载指定的数据集,选择中文语言和训练集的前500条记录 dataset = load_dataset('Conard/fortune-telling', 'default', split='train[0:200]', trust_remote_code=True) # 打印数据集的列名,查看数据集中有那些字段 print(dataset.column_names) # 定义一个函数,用于格式化数据集中的每条记录 def formatting_prompt_func(examples): # 从数据集中提取问题,复杂思考过程和回答 inputs = examples['Question'] cots = examples['Complex_CoT'] outputs = examples['Response'] texts = [] # 用于存储格式化后的文本 # 遍历每个问题,思考过程和回答,进行格式化 for input, cot, output in zip(inputs, cots, outputs): # 使用字符串模板插入数据,并加上结束标记 text = train_prompt_style.format(input, cot, output) + EOS_TOKEN texts.append(text) # 将格式化后的文本添加到列表中 return { "text": texts, # 返回包含所有格式化文本的字典 } dataset = dataset.map(formatting_prompt_func, batched=True) ret = dataset['text'][0] print(ret) model = FastLanguageModel.get_peft_model(model, # 传入已经加载好的预训练模型 r = 16, # 设置LoRA的轶,决定添加的可训练参数数量 target_modules = ["q_proj","k_proj","v_proj","o_proj", "gate_proj","up_proj","down_proj",], # 指定模型中需要微调的关键模块 lora_alpha = 16, # 设置LoRA的超参数,影响可训练参数的训练方式 lora_dropout = 0, # 设置防止过拟合的参数,这里设置为0表示不丢弃任何参数 bias = "none", # 设置是否添加偏置顶,这里为"none"表示不添加 use_gradient_checkpointing = "unsloth", # 使用优化技术节省显存并支持更大的批量大小 random_state = 3407, # 设置随机种子,确保每次运行代码时模型的初始化方式相同 use_rslora = False, # 设置是否使用 Rank Stabilized LoRA 技术, 这里设置为False表示不使用 loftq_config = None, # 设置是否使用 LoftQ技术,这里设置为None表示不适用 ) from trl import SFTTrainer # 导入SFTTrainer,用于监督式微调 from transformers import TrainingArguments # 导入TrainingArguments,用于设置训练参数 from unsloth import is_bfloat16_supported # 导入函数,检查是否支持bfloat16数据格式 # 创建一个SFTTrainer实例 trainer = SFTTrainer( model = model, # 传入要微调的模型 tokenizer=tokenizer, # 传入tokenizer, 用于处理文本数据 train_dataset=dataset, # 传入训练数据集 dataset_text_field="text", # 指定数据集中文本字段的名称 max_seq_length = max_seq_len, # 设置最大序列长度 dataset_num_proc = 2, # 设置数据处理的并行进程数 packing = False, # 是否启用打包功能(这里设置为False,打包可以让训练更快,但可能影响效果) args = TrainingArguments( # 定义训练参数 per_device_train_batch_size = 2, # 每个设备(如GPU)上的批量大小 gradient_accumulation_steps = 4, # 梯度累积步数,用于模拟大批次训练 warmup_steps = 5, # 预热步数, 训练开始时学习率逐渐增加的步数 max_steps = 75, # 最大训练步数 learning_rate = 2e-4, # 学习率,模型学习新知识的速度 fp16 = not is_bfloat16_supported(), # 是否使用fp16格式加速训练(如果环境不支持bfloat16) bf16 = is_bfloat16_supported(), # 是否使用bfloat16格式加速训练(如果格式支持) logging_steps = 1, # 每隔多少步记录一次训练日志 optim = "adamw_8bit", # 使用的优化器,用于调整模型参数 weight_decay = 0.01, # 权重衰减,防止模型过拟合 lr_scheduler_type = "linear", # 学习率调度器类型,控制学习率的变化方式 seed = 3407, # 随机种子, 确保训练结果可复现 output_dir = "outputs", # 训练结果保存的目录 report_to = "none", # 是否将训练结果报告到外部工具,这里设置为不报告 ), ) if __name__ == "__main__": trainer_stats = trainer.train()
运行结果如下(显示如下,表示正在训练,等待训练完成即可):
训练完成后,测试微调后的模型。
可以发现,回答效果专业了很多,说明本次训练是有效的。
48407
47274
38153
35414
29877
26572
25553
20499
20184
18624
32°
124°
275°
277°
249°
342°
318°
678°
413°
440°