Ai之大模型进阶模型微调-Unsloth微调-实战5.1

发布时间: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 上直接运行这些案例。

    image.png


    实战:使用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功能,双击

    image.png

    打勾  : 适用于Linux的windows子系统

    image.png


    确定后需要重启电脑。如果没有这个选项的,需要在BIOS里面设置开启虚拟化支持。

    重启后,cmd以管理员身份运行

    image.png

    命令  wsl --help

    image.png

    显示上面则安装成功。

    安装Linux系统,这里选择安装的是Ubuntu系统,也可以选其它版本的linux系统。

    命令:wsl --install -d Ubuntu

    image.png

    启动Ubuntu系统

    命令  wsl -d ubuntu

    image.png

    点击搜索ubuntu,找到文件所在位置,发送桌面快捷图标

    image.png

    直接双击打开ubuntu

    image.png

    更新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

    image.png

    安装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

    image.png

    使用conda创建虚拟python环境

    conda create --name my_unsloth_env python=3.11

    image.png

    显示下面代表成功

    image.png

    激活虚拟环境,可以看到前面变成了虚拟项目名了

    conda activate my_unsloth_env

    image.png

    安装pytorch

    conda install pytorch==2.4.0 torchvision==0.19.0 torchaudio==2.4.0 pytorch-cuda=12.1 -c pytorch -c nvidia

    image.png

    显示done表示完成

    image.png

    在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

    image.png


    加载预训练模型

    看到这里的参数是 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 

    image.png

    下载完成。


    微调前测试,先用一个算命相关的问题来测试一下,方便我们在训练完后进行对比。

    在/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])
    # 打印生成的回答部分

    结果如下:

    image.png

    没有给出答案。


    加载数据集(数据清洗)

    首先把这个数据集预期要训练出来的模型风格定义出来。

    image.png

    下面要准备一个用于微调的数据集,可以去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)

    结果如下:

    image.png

    目前所有准备工作已经完成,下一步就是开始微调训练。


    使用Unsloth执行微调

    在这一步,我们需要设置各种关键参数,把关键代码分为三段。

    第一段(模型微调准备):

    image.png

    这段代码是通过 LoRA 技术对预训练模型进行了微调准备,使其能够在特定任务上进行高效的训练,同时保留预训练模型的大部分知识。LoRA 在这篇文章中不做深度讲解,先了解即可,参数也先不用改。

    第二段(配置微调参数)

    image.png

    在这段代码中,包括一大堆参数,不需要都理解,只需要关注上面已经介绍过的三个参数:学习率(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()

    运行结果如下(显示如下,表示正在训练,等待训练完成即可):

    image.png

    训练完成后,测试微调后的模型。

    image.png

    可以发现,回答效果专业了很多,说明本次训练是有效的。


关键字