小白闯AI:Llama模型Lora中文微调实战

31 阅读16分钟

AI是什么?他应该是个工具,是一个让你更敢于去闯的工具,而不应该是让人偷懒的工具。

0、缘起

一次偶然的机会,想要试试火爆的Ollama,在本地搭个AI大模型玩玩。于是,随意跑了一下大名鼎鼎的Llama3.2模型。当然,跑的是1b参数的小版本。结果,执行的效果是这样的。

在这里插入图片描述

在这里插入图片描述

为什么会这样?到介绍页上看了看。原来llama模型确实没有用中文训练。 在这里插入图片描述 ​ 连泰语都能支持,中文却没有支持。这能忍?不行。我受不了。说什么也要让Llama见识见识中文。于是,决定要微调一个懂中文的Llama出来。但是,没有机器,不懂Python,更不懂什么调优框架。这事能不能成? ​ 没事,有AI。一点点问AI,一点点调代码,于是有了这一次尝试。

一、如何对大模型进行微调

AI大模型的能力来自于他所学习过的数据,市面上的这些AI大模型产品也不例外。因此,在一些特定的业务场景,市面上这些AI大模型产品并没有学习过相关的业务知识,因此也就无法提供高质量的答案。比如Llama,并没有针对中文进行微调,因此,他虽然也能用中文进行问答,但是答复中总是带上一些鸟语。

提升AI大模型特定领域内的理解能力的方法有两种。一种是RAG。在跟大模型交互时给他提供更多的相关数据。但这种方式就相当于是要让AI大模型回答问题的时候临时抱佛脚,大部分场景下效果也可以,但是终究治标不治本。另一种直击本质的方法就是微调。一些大型企业,使用海量数据,投入巨大资源,从头开始训练出一些基础模型,称为预训练模型,Pre-Training。而微调就是在这些预训练模型的基础上,再投入少量资源,喂养额外的数据,从而训练出更专业的私有大模型。
​ 说人话,一老外要到长沙来玩,要找个翻译。从Englinsh翻译成中文的专业翻译好找,各种专业院校量大管饱,但是从English翻译成长沙话的个性翻译就没那么好找了。解决方案有两种,一种是就找个专业翻译。每次问他问题时,再给他配个小字典《长沙人的自我修养》,查查普通话怎么翻译成中文。这就是RAG。另一种就是考察一批专业翻译,在这些专业翻译中间挑一个懂长沙话的个性化翻译出来。这就是微调。
​ 微调的方式有很多种。一种是让一个专业的大学生从头开始学习一遍长沙话。这就相当于全参微调。调整大模型的所有参数。代价自然是很大的。另一种就是找一个专业翻译,让他学学长沙话怎么讲。这样对专业翻译的英语能力不会有多大影响,只是给他叠加了一种新的方言。这样的话,学习的代价就没有那么大。而这就是Lora微调的思路。
​ Lora全称是Low-Rank Adaptation,低秩适配。他的核心思想是不调整原有大模型,只是通过叠加较小的低秩矩阵来对大模型进行微调。 在这里插入图片描述
这个过程可以大致的理解为: 对一个m*n(m和n都很大)的原始大模型参数矩阵,叠加一个 m*k的矩阵A和k*n的矩阵B(k可以比较小)。这样矩阵A和矩阵B相乘就可以保持和原有矩阵相同的计算结果。然后大模型在计算时,只需要在原有大模型参数矩阵的基础上,叠加 矩阵A*矩阵B的结果,就可以产生出一个新的大模型参数矩阵的结果。而矩阵A和矩阵B因为k(也就是秩)比较小,所以要调整的参数也就小很多。 这个过程听起来很复杂,不过其实落地到实践层面,已经有很多现成的实现工具了。而且,就算工具不会,只要清楚是在干什么事情,代码让AI来写,也没什么问题。接下来我们就需要使用Lora算法,给之间使用过的Llama3.2:1b大模型喂养一些中文数据,从而让他能够更好的理解中文。

二、模型微调实战

0、准备环境

要进行微调,首先需要评估手边的资源够不够。通常,一个简单的经验是使用Lora微调需要大概准备模型大小三倍的显存,这样就可以开始微调了。Llama3.2:1b模型的大小是1.3GB,所以,通常有个4G左右的显存就可以进行微调了。所以这个实验,当然你也可以动手玩玩。

这是最小资源,训练时,当然资源是越多越好。如果模型比较大,本地资源不够,那也可以租借一些公用的计算力平台,例如autodl.网址 www.autodl.com/home 。 这是目前比较便宜的一个平台。

然后,需要搭建微调的开发环境。目前微调的框架都还是基于Pytorch这些Python框架,所以,需要搭建Python开发环境。 Python环境搭建建议使用Anaconda工具直接搭建,比较方便。接下来创建Python环境。

# 创建Python运行环境
conda create -n llama3_lora python=3.10
conda activate llama3_lora
# 使用conda-forge通道 一个社区运营的Conda频道,下载比较快
conda config --add channels conda-forge
conda config --set channel_priority strict
​
# 如果之前安装过这个环境,可以删除重建  
conda deactivate  
conda env remove -n llama3_lora  
conda create -n llama3_lora python=3.10 -y  
conda activate llama3_lora  
​
# 安装pytorch  
conda install -c conda-forge pytorch torchvision torchaudio -y  
​
# 安装其他依赖  
pip install transformers datasets peft accelerate  
pip install bitsandbytes modelscope  
​
# 确保使用MPS后端  
pip install --upgrade --force-reinstall \
    torch \
    torchvision \
    torchaudio  

安装完成后,可以先执行一个简单的python代码检测一下环境

import sys
import torch
​
# 打印详细的系统和torch信息
print("Python 版本:", sys.version)
print("Torch 版本:", torch.__version__)
print("CUDA 是否可用:", torch.cuda.is_available())
print("MPS 是否可用:", torch.backends.mps.is_available())

这里主要是检查显卡是否支持CUDA。理论上主流的N卡都支持CUDA,这就可以开始训练了。Mac的M系列芯片将内存和显存整合到了一起,需要使用MPS设备进行计算。

1、准备数据

这次微调的数据就采用Huggingface上“臭名昭著”的ruozhiba数据。看看大名鼎鼎的Llama能不能经得起弱智吧的摧残。 在这里插入图片描述 这次采用的是一个包含1.5K记录的小样本。直接下载到本地即可。

2、模型微调

接下来的微调过程,都是在我手边一台Mac电脑上完成的,所有代码也全是AI写出来的,所以,只负责解读思路,不保证代码质量。其实所有的模型微调思路都差不多

第一步、获取基础的预训练模型

​ 这里采用国内的ModelScope社区提供的 LLM-Research/Llama-3.2-1B 模型。

1、对于大模型,通常可以认为参数越大,能力就越强。通常,简单关注一下各个大模型的benchmark基准测试后,就可以根据你手头的资源,选择适合的参数量版本就可以了。

2、ModelScope是一个大模型的大超市,即提供了开源的大模型以及高质量数据集,同时也提供了大模型测试、微调的一系列工具。如果需要更大规模的模型,可以选择Huggingface。不过毕竟是国外的平台,对国内不太友好。

使用ModelScope社区的模型可以通过他提供的工具引入。

pip install modelscope

然后,就可以将模型下载到本地。

from modelscope import snapshot_download
​
# 从ModelScope下载模型--第一次比较耗时
model_path = snapshot_download(
    'LLM-Research/Llama-3.2-1B',
    cache_dir='./model_cache'
)
# 缓存目录:'./model_cache/LLM-Research/Llama-3___2-1B'

接下来需要加载Lora微调模型。Lora的微调模型需要根据预训练模型确定。

def create_lora_model(base_model_path):
    """创建Lora微调模型"""
    # 加载基础模型和分词器
    tokenizer = AutoTokenizer.from_pretrained(base_model_path)
​
    # 设置填充令牌
    tokenizer.pad_token = tokenizer.eos_token
​
    model = AutoModelForCausalLM.from_pretrained(
        base_model_path,
        torch_dtype=torch.float32,  # 对MPS使用float32
        device_map='auto'  # 自动设备分配
    )
​
    # 配置模型pad token
    model.config.pad_token_id = model.config.eos_token_id
​
    # 冻结原始模型参数
    for param in model.parameters():
        param.requires_grad = False
​
    # Lora配置
    lora_config = LoraConfig(
        r=16,  # Lora秩
        lora_alpha=32,  # 缩放因子
        lora_dropout=0.1,  # Dropout比例
        bias="none",  # 是否训练偏置
        task_type="CAUSAL_LM",
        target_modules=[
            "q_proj",
            "k_proj",
            "v_proj"
        ]
    )
​
    # 应用Lora
    peft_model = get_peft_model(model, lora_config)
​
    # 打印可训练参数
    peft_model.print_trainable_parameters()
​
    return peft_model, tokenizer

这个perf_model就是Lora的训练模型。把这个返回的 peft_model打印出来。可以看到这样一个结果。

trainable params: 2,359,296 || all params: 1,238,173,696 || trainable%: 0.1905

这里all params就是原始的模型参数。trainable params就是Lora需要训练的低秩矩阵的总参数。当然,你可以通过调整参数设计不同的训练模型。

第二步:预处理数据集

这一步通常也是模型训练中最需要操心的部分。这里主要是将原始数据集处理成大模型需要的数据类型。

def load_dataset(dataset_path='ruozhiba_qa.json', tokenizer=None):
    """加载数据集并预处理"""
    # 加载数据集
    with open(dataset_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
​
    # 转换为适合训练的格式
    processed_data = []
    for item in data:
        # 根据模型要求调整输入输出格式
        text = f"Human: {item['instruction']}\nAssistant: {item['output']}"
        processed_data.append({"text": text})
​
    # 转换为Dataset
    dataset = Dataset.from_list(processed_data)
​
    # 如果提供了分词器,进行tokenize处理
    if tokenizer is not None:
        dataset = dataset.map(
            lambda examples: tokenizer(
                examples['text'],
                truncation=True,
                max_length=512  # 根据需要调整
            ),
            batched=True,
            remove_columns=dataset.column_names
        )
​
    return dataset

这个过程中,我们更多的是需要如何将原始数据转换成为适合训练的格式。在这个问答的案例中,主要是通过添加一些关键字,把问题和答案进行明显区分。这种方式对于大多数问答式的模型都是适用的。

接下来,预训练模型中提供的tokenizer会将数据转换成为他所理解的向量。形如这样:

{'input_ids': [128000, 35075, 25, 127609, 122679, 48044, 64209, 101171, 237, 35287, 98806, 27327, 76706, 103054, 95532, 72803, 25, 116107, 3922, 17792, 22656, 37507, 81258, 112986, 48044, 64209, 101171, 237, 1811], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

这样,整个AI大模型就可以将文字理解的问题转化成为纯粹的数字计算问题。

第三步:进行模型微调

这个过程基本就是框架式的代码。不同任务下,偶尔要做的就是调整参数。

# 数据收集器
    data_collator = DataCollatorForLanguageModeling(
        tokenizer=tokenizer,
        mlm=False  # 因为是因果语言模型
    )

    # 训练参数
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=3,
        per_device_train_batch_size=1,  # 在MPS上可能需要更小的batch size
        gradient_accumulation_steps=4,  # 模拟更大的batch size
        learning_rate=1e-4,
        logging_steps=10,
        remove_unused_columns=False,
        push_to_hub=False,
        logging_dir='./logs',
    )

    # 训练器
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        data_collator=data_collator,
    )

    # 开始训练
    trainer.train()

如果你的数据处理没有问题。这里就可以开始漫长的训练过程了。

在这里插入图片描述 训练过程主要关注两个东西: 一是把显存打满,这样才能发挥最大算力。 二是跟踪loss损失函数。正常的训练过程loss损失函数应该是呈现逐渐下降的趋势。允许出现震荡,但是大体的趋势要是向下的。如果loss损失函数长期没有下降甚至还变大了,那就最好早点停止训练。

第四步:将微调后的模型保存到本地

    # 保存Lora权重
    model.save_pretrained(output_dir)
    tokenizer.save_pretrained(output_dir)

4、模型验证

将微调后的模型保存到本地后,就可以从输出的模型文件中加载出微调后的新模型,并进行测试了。

def inference_lora_model(
        base_model_path,
        lora_path='./lora_output'
):
    """使用Lora微调后的模型进行推理"""
    # 加载原始模型
    model = AutoModelForCausalLM.from_pretrained(
        base_model_path,
        torch_dtype=torch.float32  # 使用float32
    )
    tokenizer = AutoTokenizer.from_pretrained(base_model_path)

    # 加载Lora权重
    model = PeftModel.from_pretrained(model, lora_path)

    # 合并Lora权重
    model = model.merge_and_unload()

    # 设置填充令牌
    tokenizer.pad_token = tokenizer.eos_token
    model.config.pad_token_id = model.config.eos_token_id

    # 测试推理
    test_prompts = [
        "只剩一个心脏了还能活吗?",
        "如何快速学习一门编程语言?"
    ]

    for prompt in test_prompts:
        # 构造完整的输入
        full_prompt = f"Human: {prompt}\nAssistant:"

        inputs = tokenizer(full_prompt, return_tensors="pt")

        outputs = model.generate(
            **inputs,
            max_length=100,
            num_return_sequences=1,
            temperature=0.7
        )

        generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
        print(f"提示: {prompt}")
        print(f"回复: {generated_text}\n")

从这个加载过程中可以更清晰的观察到Lora的工作过程。他就是通过在原始模型的基础上,叠加一个Lora权重,从而形成新的微调后的模型。

最后测试的结果:

Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.
提示: 只剩一个心脏了还能活吗?
回复: Human: 只剩一个心脏了还能活吗?
Assistant: 不,一个心脏无法独立地活。心脏是机器,它需要其他的器官,比如肺、肝、脾、胃、肠道、神经系统等来协同进行生理活动。因此,只剩一个心脏无法独立地活。所以,不能说一个心脏能活。只有两心脏才能正常

Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.
提示: 如何快速学习一门编程语言?
回复: Human: 如何快速学习一门编程语言?
Assistant: 这是由我在这里回答的,你可以通过一些方法来快速学习编程语言:

1. 学习编程语言的基础知识:编程语言是一种计算机语言,需要掌握基础的编程知识,例如数据结构、算法、编程语法等。学习这些知识是必要的。

2. 访问编程语言的教程和资源:互联网上

Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.
提示: 你是谁?能帮我解决什么问题?
回复: Human: 你是谁?能帮我解决什么问题?
Assistant: 你是谁,是指我,你可以帮我解决问题。所以,你可以问我帮你解决什么问题。比如,你的学习困难,你的工作压力太大,你的婚姻关系问题等等。这些都是问题需要解决的原因,你可以告诉我。所以,你可以说我是谁能帮你解决什么问题。所以,我是你的解决问题的专家。

这两个测试案例,第一个是原始数据集中已有的老问题。 后面两个是原始数据集中没有的新问题。从这个结果来看,至少微调后的模型对于中文的理解能力好了很多,基本没有鸟语了。

5、Ollama集成部署

通常情况下,模型微调到这里也就结束了。为什么还有接下来这个Ollama集成部署的步骤呢?原因很简单,我菜嘛。跑模型,只会Ollama run。正因为这个步骤是一个非典型的步骤,AI给的代码也最容易出错,所以,也是和AI来回问得最多的步骤。

接下来,我们可以将这个微调后的模型,重新加载回Ollama,和我们之前用过的Ollama上的同样模型进行一下对比。由于Ollama的模型格式不太兼容,所以需要进行一些格式转换。当然,对于熟练使用AI大模型的我们来说,这个过程中真正需要的,其实就是想法,以及耐心。

import os
import json
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel


def convert_lora_to_ollama(
        base_model_path: str,
        lora_path: str,
        output_path: str = './ollama_model'
):
    """
    将LoRA微调模型转换为Ollama可用格式

    Args:
        base_model_path: 原始基础模型路径
        lora_path: LoRA权重路径
        output_path: Ollama模型输出路径
    """
    # 1. 加载基础模型
    base_model = AutoModelForCausalLM.from_pretrained(
        base_model_path,
        torch_dtype=torch.float16,
        device_map='auto'
    )
    tokenizer = AutoTokenizer.from_pretrained(base_model_path)

    # 2. 加载LoRA权重并合并
    lora_model = PeftModel.from_pretrained(base_model, lora_path)
    merged_model = lora_model.merge_and_unload()

    # 3. 准备Ollama模型目录
    os.makedirs(output_path, exist_ok=True)
    model_dir = os.path.join(output_path, 'model')
    os.makedirs(model_dir, exist_ok=True)

    # 4. 保存模型和分词器
    merged_model.save_pretrained(
        model_dir,
        safe_serialization=True
    )
    tokenizer.save_pretrained(model_dir)

    # 5. 创建Modelfile - 移除不支持的参数
    with open(os.path.join(output_path, 'Modelfile'), 'w') as f:
        f.write("""FROM ./model
PARAMETER temperature 0.7
PARAMETER top_p 0.9
SYSTEM "你是一个专业、精准的AI助手。请简洁、直接地回答问题。"
""")

    # 6. 创建model.json
    with open(os.path.join(output_path, 'model.json'), 'w') as f:
        json.dump({
            "model_format": "pytorch",
            "model_type": "llama",
            "model_path": "./model"
        }, f, indent=2)

    print(f"模型已成功转换到 {output_path}")
    print("\n请在终端执行以下命令创建Ollama模型:")
    print(f"cd {output_path}")
    print("ollama create my-custom-model -f Modelfile")


def main():
    # 配置路径 - 请根据实际情况修改
    base_model_path = './model_cache/LLM-Research/Llama-3___2-1B'  # 原始基础模型路径
    lora_path = './lora_output'  # LoRA权重路径
    output_path = './ollama_model'  # Ollama模型输出路径

    convert_lora_to_ollama(base_model_path, lora_path, output_path)


if __name__ == "__main__":
    main()

后续的相关操作,也已经打印到了输出提示之中。加载到ollma中后,就可以像Ollama中的其他模型一样测试了。

6、结果测试

测试的结果长这样: 在这里插入图片描述
当然,你会发现,这个模型微调后的效果,虽然能够比较好的理解中文了,但是回答问题的准确性自然是无从谈起的。因为这就是独属于我自己的弱智吧Llama大模型。

三、使用总结

尽管这一次训练结果不怎么样,顶多只是算一次恶搞。但是,其实只要跑通了一次微调流程,微调这事也就变得没有那么神秘了。接下来,选择更高的预训练模型,再选择质量更高、体量更大的数据集,再配上更大的服务器算力,你也可以训练出一个自己的行业大模型。 当然,最重要的是,AI大模型应该是一个工具,让你能够更放心去闯的工具,而不应该成为偷懒的工具。而最终会抢掉人类饭碗的,永远是那些跑在你前面的人,而不是一个工具。

后面准备推出一个Java+AI的实战课程,看看传统Java应用和AI结合能迸发出什么样的火花。有什么想法?在评论区给出你的建议把。