Phi-4 FastApi 部署调用
环境准备
本文基础环境如下:
----------------
ubuntu 22.04
python 3.12
cuda 12.1
pytorch 2.3.0
----------------
本文默认学习者已安装好以上 Pytorch(cuda) 环境,如未安装请自行安装。
pip 换源加速下载并安装依赖包
# 升级pip
python -m pip install --upgrade pip
# 更换 pypi 源加速库的安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install transformers==4.44.2
pip install huggingface-hub==0.25.0
pip install accelerate==0.34.2
pip install modelscope==1.18.0
pip install fastapi==0.115.1
pip install uvicorn==0.30.6
考虑到部分同学配置环境可能会遇到一些问题,我们在AutoDL平台准备了Phi-4的环境镜像,点击下方链接并直接创建Autodl示例即可。 https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-phi4
模型下载
使用魔搭社区中的 modelscope 中的 snapshot_download 函数下载模型,第一个参数为模型名称(如何找到该名称?可以在魔搭社区搜该模型,如下图中所框),参数 cache_dir 为模型的下载路径,参数revision一般默认为master。
在/root/autodl-tmp 新建 model_download.py 文件并在其中输入以下内容,粘贴代码后记得保存文件,如下所示。并运行 python model_download.py 执行下载,模型大小为 28 GB左右,下载模型大概需要10到 20 分钟。
import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
model_dir = snapshot_download('LLM-Research/phi-4', cache_dir='/root/autodl-tmp', revision='master')
注意:记得修改
cache_dir为你的模型下载路径哦~
代码准备
在 /root/autodl-tmp 路径下新建 api.py 文件并在其中输入以下内容,粘贴代码后请及时保存文件。 下面的代码有很详细的注释,大家如有不理解的地方,欢迎提出 issue。
from fastapi import FastAPI, Request
from transformers import AutoTokenizer, AutoModelForCausalLM
import uvicorn
import json
import datetime
import torch
# 创建FastAPI应用
app = FastAPI()
# 处理POST请求的端点
@app.post("/")
async def create_item(request: Request):
global model, tokenizer # 声明全局变量以便在函数内部使用模型和分词器
json_post_raw = await request.json() # 获取POST请求的JSON数据
print("json_post_raw", json_post_raw)
prompt = json_post_raw.get('prompt') # 获取请求中的提示
history = json_post_raw.get('history', []) # 获取请求中的历史消息,默认为空列表
# 构建消息列表,包括历史消息和当前提示
messages = history + [{"role": "user", "content": prompt}]
# 调用模型进行对话生成
input_ids = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
model_inputs = tokenizer([input_ids], return_tensors="pt").to(model.device)
generated_ids = model.generate(model_inputs.input_ids, max_new_tokens=512)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
# 更新历史消息,将当前对话添加到历史中
updated_history = messages + [{"role": "assistant", "content": response}]
now = datetime.datetime.now() # 获取当前时间
time = now.strftime("%Y-%m-%d %H:%M:%S") # 格式化时间为字符串
# 构建响应JSON
answer = {
"response": response,
"history": updated_history, # 返回更新后的历史消息
"status": 200,
"time": time
}
# 构建日志信息
log = "[" + time + "] " + '", prompt:"' + prompt + '", response:"' + repr(response) + '"'
print(log) # 打印日志
return answer # 返回响应
# 主函数入口
if __name__ == '__main__':
# 加载预训练的分词器和模型
model_name_or_path = '/root/autodl-tmp/LLM-Research/phi-4'
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, use_fast=False)
tokenizer.pad_token_id = tokenizer.eos_token_id = 100265
model = AutoModelForCausalLM.from_pretrained(model_name_or_path, device_map="auto", torch_dtype=torch.bfloat16)
# 启动FastAPI应用
# 用6006端口可以将autodl的端口映射到本地,从而在本地使用api
uvicorn.run(app, host='0.0.0.0', port=6006, workers=1) # 在指定端口和主机上启动应用
Api 部署
启动api服务
在终端输入以下命令
cd /root/autodl-tmp
python api.py
python /root/api.py
默认部署在 6006 端口,加载完毕后出现如下信息说明成功。
(可选)AutoDL平台-SSH端口映射
ssh -CNg -L 6006:127.0.0.1:6006 -p 【你的autodl机器的ssh端口】 root@[你的autodl机器地址]
ssh -CNg -L 6006:127.0.0.1:6006 -p 36494 root@region-45.autodl.pro
调用Api
方式1:Curl 调用
通过 POST 方法进行调用,可以使用 curl 调用,如下所示:
curl -X POST "http://127.0.0.1:6006" -H "Content-Type: application/json" -d "{\"prompt\": \"你好\", \"history\": []}"
方式2:Python requests库调用
代码准备
在/root/autodl-tmp路径下新建 py_api.py 文件并在其中输入以下内容,粘贴代码后记得保存文件。
import requests
url = "http://127.0.0.1:6006/"
# 参数history可以为空列表,即[],此时代表没有历史对话
payload = {
"prompt": "刚才我们再聊什么?",
"history": [
{
"role": "user",
"content": "今天星期几"
},
{
"role": "assistant",
"content": "今天星期三"
},
{
"role": "user",
"content": "明天星期几?"
},
{
"role": "assistant",
"content": "明天是星期四。"
}
]
}
headers = {
"Content-Type": "application/json",
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.94 Safari/537.37",
"Connection": "keep-alive"
}
response = requests.request("POST", url, json=payload, headers=headers)
print(response.text)
运行代码
在终端输入以下命令运行代码:
python /root/autodl-tmp/py_api.py
调用结果:
方式3:ApiPost 调用
使用 ApiPost软件 测试 history 的用法:
Header 参数:
Content-Type: application/json
Json 参数:
{
"prompt": "刚才我们再聊什么?",
"history": [
{
"role": "user",
"content": "今天星期几"
},
{
"role": "assistant",
"content": "今天星期三"
},
{
"role": "user",
"content": "明天星期几?"
},
{
"role": "assistant",
"content": "明天是星期四。"
}
]
}
图示效果:
Phi-4 Langchain接入
环境准备
本文基础环境如下:
----------------
ubuntu 22.04
python 3.12
cuda 12.1
pytorch 2.3.0
----------------
本文默认学习者已安装好以上 Pytorch(cuda) 环境,如未安装请自行安装。
pip 换源加速下载并安装依赖包
# 升级pip
python -m pip install --upgrade pip
# 更换 pypi 源加速库的安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install transformers==4.44.2
pip install huggingface-hub==0.25.0
pip install accelerate==0.34.2
pip install modelscope==1.18.0
pip install langchain==0.3.0
考虑到部分同学配置环境可能会遇到一些问题,我们在AutoDL平台准备了Phi-4的环境镜像,点击下方链接并直接创建Autodl示例即可。 https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-phi4
模型下载
使用魔搭社区中的 modelscope 中的 snapshot_download 函数下载模型,第一个参数为模型名称(如何找到该名称?可以在魔搭社区搜该模型,如下图中所框),参数 cache_dir 为模型的下载路径,参数revision一般默认为master。
在/root/autodl-tmp 新建 model_download.py 文件并在其中输入以下内容,粘贴代码后记得保存文件,如下所示。并运行 python model_download.py 执行下载,模型大小为 28 GB左右,下载模型大概需要10到 20 分钟。
import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
model_dir = snapshot_download('LLM-Research/phi-4', cache_dir='/root/autodl-tmp', revision='master')
注意:记得修改
cache_dir为你的模型下载路径哦~
代码准备
为便捷构建 LLM 应用,我们需要基于本地部署的 Phi_4_LLM,自定义一个 LLM 类,(这个类主要用于加载和调用一个基于本地的预训练语言模型,如Phi_4,并根据1给定的提示生成文本响应)将 Phi_4 接入到 LangChain 框架中。完成自定义 LLM 类之后,可以以完全一致的方式调用 LangChain 的接口,而无需考虑底层模型调用的不一致。
基于本地部署的 Phi_4 自定义 LLM 类并不复杂,我们只需从 LangChain.llms.base.LLM 类继承一个子类,并重写构造函数与 _call 函数即可:
在当前路径新建一个 LLM.py 文件,并输入以下内容,粘贴代码后记得保存文件。
from langchain.llms.base import LLM #基础类,用于实现自定义的语言模型
from typing import Any, List, Optional
from langchain.callbacks.manager import CallbackManagerForLLMRun #回调管理器,用于处理在模型运行期间的事件
from transformers import AutoTokenizer, AutoModelForCausalLM, GenerationConfig, LlamaTokenizerFast #Hugging Face 提供的库,用于加载预训练的 NLP 模型
import torch
class Phi_4_LLM(LLM):
# 基于本地 Phi_4 自定义 LLM 类
tokenizer: AutoTokenizer = None #tokenizer:用于将输入文本转换为模型可以理解的 token
model: AutoModelForCausalLM = None #model:预训练的语言模型
def __init__(self, mode_name_or_path :str): #__init__ 方法初始化模型和分词器
super().__init__()
print("正在从本地加载模型...")
self.tokenizer = AutoTokenizer.from_pretrained(mode_name_or_path, use_fast=False) #使用 AutoTokenizer.from_pretrained 加载分词器
self.tokenizer.pad_token_id = self.tokenizer.eos_token_id = 100265
self.model = AutoModelForCausalLM.from_pretrained(mode_name_or_path, torch_dtype=torch.bfloat16, device_map="auto") #使用 AutoModelForCausalLM.from_pretrained 加载预训练的因果语言模型,并设置数据类型为 bfloat16,使用自动设备分配策略。
self.model.generation_config = GenerationConfig.from_pretrained(mode_name_or_path) #设置生成配置
print("完成本地模型的加载")
def _call(self, prompt : str, stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any): #_call 方法用于生成文本响应
messages = [{"role": "user", "content": prompt }] #构造消息列表,包含用户的角色和提示内容
input_ids = self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) #使用 apply_chat_template 方法应用聊天模板,并获取输入 ID
model_inputs = self.tokenizer([input_ids], return_tensors="pt").to(self.model.device) #将输入 ID 转换为 PyTorch 张量,并移动到 GPU 上
generated_ids = self.model.generate(model_inputs.input_ids, attention_mask=model_inputs['attention_mask'], max_new_tokens=512) #使用 generate 方法生成新的 token
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
] #处理生成的 token,移除输入部分,只保留新生成的部分
response = self.tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
return response #将生成的 token 解码为文本响应,并返回
@property
def _llm_type(self) -> str:
return "Phi_4"
在上述类定义中,我们分别重写了构造函数和 _call 函数:对于构造函数,我们在对象实例化的一开始加载本地部署的 Phi_4 模型,从而避免每一次调用都需要重新加载模型带来的时间过长;_call 函数是 LLM 类的核心函数,LangChain 会调用该函数来调用 LLM,在该函数中,我们调用已实例化模型的 generate 方法,从而实现对模型的调用并返回调用结果。
在整体项目中,我们将上述代码封装为 LLM.py,后续将直接从该文件中引入自定义的 LLM 类。
调用
然后就可以像使用任何其他的langchain大模型功能一样使用了。
注意:记得修改模型路径为你的路径哦~
from LLM import Phi_4_LLM
llm = Phi_4_LLM(mode_name_or_path = "/root/autodl-tmp/LLM-Research/phi-4")
print(llm("你是谁"))
报错
在调用的时候我出现了一个报错如下图所示:
报错原因是我一开始在LLM.py文件中写的类名是Phi_4,然后from LLM import Phi_4_LLM 这行代码的作用是从 LLM 模块中导入 Phi_4_LLM 类,将这两者保持一致即可。所以将Phi_4修改为Phi_4_LLM后就调用成功了~嘻嘻
Phi-4 WebDemo 部署
环境准备
本文基础环境如下:
----------------
ubuntu 22.04
python 3.12
cuda 12.1
pytorch 2.3.0
----------------
本文默认学习者已安装好以上 Pytorch(cuda) 环境,如未安装请自行安装。
pip 换源加速下载并安装依赖包
# 升级pip
python -m pip install --upgrade pip
# 更换 pypi 源加速库的安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install transformers==4.44.2
pip install huggingface-hub==0.25.0
pip install accelerate==0.34.2
pip install modelscope==1.18.0
pip install streamlit==1.41.1
考虑到部分同学配置环境可能会遇到一些问题,我们在AutoDL平台准备了Phi-4的环境镜像,点击下方链接并直接创建Autodl示例即可。 https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-phi4
模型下载
使用魔搭社区中的 modelscope 中的 snapshot_download 函数下载模型,第一个参数为模型名称(如何找到该名称?可以在魔搭社区搜该模型,如下图中所框),参数 cache_dir 为模型的下载路径,参数revision一般默认为master。
在/root/autodl-tmp 新建 model_download.py 文件并在其中输入以下内容,粘贴代码后记得保存文件,如下所示。并运行 python model_download.py 执行下载,模型大小为 28 GB左右,下载模型大概需要10到 20 分钟。
代码准备
在/root/autodl-tmp路径下新建 chatBot.py 文件并在其中输入以下内容,粘贴代码后记得保存文件。下面的代码有很详细的注释,大家如有不理解的地方,欢迎提出issue。
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
import streamlit as st
# 在侧边栏中创建一个标题和一个链接
with st.sidebar:
st.markdown("## Phi4 LLM")
"[开源大模型食用指南 self-llm](https://github.com/datawhalechina/self-llm.git)"
# 创建一个滑块,用于选择最大长度,范围在 0 到 8192 之间,默认值为 512(Qwen2.5 支持 128K 上下文,并能生成最多 8K tokens)
max_length = st.slider("max_length", 0, 8192, 512, step=1)
# 创建一个标题和一个副标题
st.title("💬 Phi4 Chatbot")
st.caption("🚀 A streamlit chatbot powered by Self-LLM")
# 定义模型路径
mode_name_or_path = '/root/autodl-tmp/LLM-Research/phi-4'
# 定义一个函数,用于获取模型和 tokenizer
@st.cache_resource
def get_model():
# 从预训练的模型中获取 tokenizer
tokenizer = AutoTokenizer.from_pretrained(mode_name_or_path, trust_remote_code=True)
tokenizer.pad_token_id = tokenizer.eos_token_id = 100265
# 从预训练的模型中获取模型,并设置模型参数
model = AutoModelForCausalLM.from_pretrained(mode_name_or_path, torch_dtype=torch.bfloat16, device_map="auto")
return tokenizer, model
# 加载 Qwen2.5 的 model 和 tokenizer
tokenizer, model = get_model()
# 如果 session_state 中没有 "messages",则创建一个包含默认消息的列表
if "messages" not in st.session_state:
st.session_state["messages"] = [{"role": "assistant", "content": "有什么可以帮您的?"}]
# 遍历 session_state 中的所有消息,并显示在聊天界面上
for msg in st.session_state.messages:
st.chat_message(msg["role"]).write(msg["content"])
# 如果用户在聊天输入框中输入了内容,则执行以下操作
if prompt := st.chat_input():
# 在聊天界面上显示用户的输入
st.chat_message("user").write(prompt)
# 将用户输入添加到 session_state 中的 messages 列表中
st.session_state.messages.append({"role": "user", "content": prompt})
# 将对话输入模型,获得返回
input_ids = tokenizer.apply_chat_template(st.session_state.messages,tokenize=False,add_generation_prompt=True)
model_inputs = tokenizer([input_ids], return_tensors="pt").to(model.device)
generated_ids = model.generate(model_inputs.input_ids,max_new_tokens=max_length)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
# 将模型的输出添加到 session_state 中的 messages 列表中
st.session_state.messages.append({"role": "assistant", "content": response})
# 在聊天界面上显示模型的输出
st.chat_message("assistant").write(response)
# print(st.session_state) # 打印 session_state 调试
运行 demo
在终端中运行以下命令,启动streamlit服务,并按照 autodl 的指示将端口映射到本地,然后在浏览器中打开链接 http://localhost:6006/ ,即可看到聊天界面。
streamlit run /root/autodl-tmp/chatBot.py --server.address 127.0.0.1 --server.port 6006
Phi-4 Lora 微调
环境配置
本文基础环境如下:
----------------
ubuntu 22.04
Python 3.12.3
cuda 12.1
pytorch 2.3.0
----------------
本文默认学习者已安装好以上 Pytorch(cuda) 环境,如未安装请自行安装。
首先 pip 换源加速下载并安装依赖包:
# 升级pip
python -m pip install --upgrade pip
# 更换 pypi 源加速库的安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install modelscope==1.22.0 # 用于模型下载和管理
pip install transformers==4.47.1 # Hugging Face 的模型库,用于加载和训练模型
pip install streamlit==1.41.1
pip install sentencepiece==0.2.0
pip install accelerate==0.34.2 # 用于分布式训练和混合精度训练
pip install datasets==2.20.0 # 用于加载和处理数据集
pip install peft==0.11.1 # 用于 LoRA 微调
考虑到部分同学配置环境可能会遇到一些问题,我们在AutoDL平台准备了Phi-4的环境镜像,点击下方链接并直接创建Autodl示例即可。 https://www.codewithgpu.com/i/datawhalechina/self-llm/self-llm-phi4
模型下载
modelscope 是一个模型管理和下载工具,支持从 Hugging Face 等平台快速下载模型。
这里使用 modelscope 中的 snapshot_download 函数下载模型,第一个参数为模型名称,第二个参数 cache_dir 为模型的下载路径,第三个参数 revision 为模型的版本号。
在 /root/autodl-tmp 路径下新建 model_download.py 文件并在其中粘贴以下代码,请及时保存文件。在终端运行 python /root/autodl-tmp/model_download.py 执行下载,模型大小为 28GB,下载模型大概需要20分钟左右。
注意不要在notebook中直接运行哦~
import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
model_dir = snapshot_download('LLM-Research/phi-4', cache_dir='/root/autodl-tmp', revision='master')
注意:记得修改 cache_dir 为你的模型下载路径哦~
指令集构建
LLM 的微调一般指指令微调过程。所谓指令微调,是说我们使用的微调数据形如:
{
"instruction": "回答以下用户问题,仅输出答案。",
"input": "1+1等于几?",
"output": "2"
}
其中,instruction 是用户指令,告知模型其需要完成的任务;input 是用户输入,是完成用户指令所必须的输入内容;output 是模型应该给出的输出。
即我们的核心训练目标是让模型具有理解并遵循用户指令的能力。因此,在指令集构建时,我们应针对我们的目标任务,针对性构建任务指令集。例如,在本节我们使用由笔者合作开源的 Chat-甄嬛 项目作为示例,我们的目标是构建一个能够模拟甄嬛对话风格的个性化 LLM,因此我们构造的指令形如:
{
"instruction": "你是谁?",
"input": "",
"output": "家父是大理寺少卿甄远道。"
}
我们所构造的全部指令数据集在根目录下。
数据格式化
Lora 训练的数据是需要经过格式化、编码之后再输入给模型进行训练的,如果是熟悉 Pytorch 模型训练流程的同学会知道,我们一般需要将输入文本编码为 input_ids,将输出文本编码为 labels,编码之后的结果都是多维的向量。在这里我们首先定义一个预处理函数,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典,方便模型使用:
def process_func(example):
MAX_LENGTH = 384 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性
input_ids, attention_mask, labels = [], [], []
# 构建指令部分的输入
instruction = tokenizer(
f"<|im_start|>system\n现在你要扮演皇帝身边的女人--甄嬛<|im_end|>\n"
f"<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n"
f"<|im_start|>assistant\n",
add_special_tokens=False
)
# 构建模型回复部分的输入
response = tokenizer(
f"{example['output']}",
add_special_tokens=False
)
# 拼接指令和回复部分的 input_ids
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
# 拼接指令和回复部分的 attention_mask
attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为 EOS token 也需要关注,所以补充为 1
# 构建标签
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id] # 对于指令部分,使用 -100 忽略其损失计算;对于回复部分,保留其 input_ids 作为标签
# 如果总长度超过最大长度,进行截断
if len(input_ids) > MAX_LENGTH:
input_ids = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels
}
Phi-4 采用的 Prompt Template格式如下:
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
你是谁?<|im_end|>
<|im_start|>assistant
我是一个有用的助手。<|im_end|>
加载 tokenizer 和半精度模型
tokenizer 是将文本转换为模型能理解的数字的工具,model 是根据这些数字生成文本的核心部分。
model 以半精度形式加载, 如果你的显卡比较新的话,可以用 torch.bfolat 形式加载。对于自定义模型,必须指定 trust_remote_code=True ,以确保加载自定义代码时不会报错。
tokenizer = AutoTokenizer.from_pretrained('/root/autodl-tmp/LLM-Research/phi-4', use_fast=False, trust_remote_code=True)
tokenizer.pad_token_id = tokenizer.eos_token_id = 100265 # 100265 == '<|im_end|>'
model = AutoModelForCausalLM.from_pretrained('/root/autodl-tmp/LLM-Research/phi-4', device_map="auto",torch_dtype=torch.bfloat16)
注意:此处要记得修改为自己的模型路径哦~
定义 LoraConfig
LoraConfig这个类中可以设置很多参数,但主要的参数没多少,简单讲一讲,感兴趣的同学可以直接看源码。
task_type:模型类型
target_modules:需要训练的模型层的名字,主要就是 attention部分的层,不同的模型对应的层的名字不同,可以传入数组,也可以字符串,也可以正则表达式。
r:lora的秩,具体可以看 Lora原理。
lora_alpha:Lora alaph ,具体作用参见 Lora 原理。
lora_dropout: Lora 层的 Dropout 比例,用于防止过拟合,具体作用参见 Lora 原理。
Lora的缩放是啥嘞?当然不是 r(秩),这个缩放就是 lora_alpha/r, 在这个 LoraConfig中缩放就是 4 倍。
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
inference_mode=False, # 训练模式
r=8, # Lora 秩
lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
lora_dropout=0.1# Dropout 比例
)
自定义 TrainingArguments 参数
TrainingArguments这个类的源码也介绍了每个参数的具体作用,当然大家可以来自行探索,这里就简单说几个常用的。
output_dir:模型的输出路径
per_device_train_batch_size:顾名思义 batch_size,批量大小
gradient_accumulation_steps: 梯度累加,如果你的显存比较小,那可以把 batch_size 设置小一点,梯度累加增大一些。
logging_steps:多少步,输出一次 log
num_train_epochs:顾名思义 epoch,训练轮次
gradient_checkpointing:梯度检查,这个一旦开启,模型就必须执行 model.enable_input_require_grads(),这个原理大家可以自行探索,这里就不细说了。
args = TrainingArguments(
output_dir="./output/phi4_lora",
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
logging_steps=10,
num_train_epochs=3,
save_steps=100,
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True
)
使用 Trainer 训练
我们使用 Trainer 类来管理训练过程。TrainingArguments 用于设置训练参数,Trainer 则负责实际的训练逻辑。
trainer = Trainer(
model=model, # 要训练的模型
args=args, # 训练参数
train_dataset=tokenized_id, # 训练数据集
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), # 数据整理器
)
trainer.train() # 开始训练
训练大概要30分钟左右哦~
加载 lora 权重推理
训练好了之后可以使用如下方式加载 lora权重进行推理:
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from peft import PeftModel
mode_path = '/root/autodl-tmp/LLM-Research/phi-4'
lora_path = 'output/phi4_lora/checkpoint-300' # 这里改称你的 lora 输出对应 checkpoint 地址
# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(mode_path, trust_remote_code=True)
# 加载模型
model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.bfloat16, trust_remote_code=True).eval()
# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=lora_path)
prompt = "你是谁?"
inputs = tokenizer.apply_chat_template([
{"role": "system", "content": "现在你要扮演皇帝身边的女人--甄嬛"},
{"role": "user", "content": prompt}
],
add_generation_prompt=True,
tokenize=True,
return_tensors="pt",
return_dict=True
).to(model.device) # 这里一定要注意将 inputs 移动到模型所在的设备,确保设备一致性
gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1}
with torch.no_grad():
outputs = model.generate(**inputs, **gen_kwargs)
outputs = outputs[:, inputs['input_ids'].shape[1]:]
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
注意修改为自己的模型路径哦~‘
如果显示
Some parameters are on the meta device because they were offloaded to the cpu.的报错,需要将实例关机,重启后单独运行本条代码。
Phi-4 Lora 微调 命名实体识别任务 SwanLab 可视化记录版
本节我们简要介绍如何基于 transformers、peft 等框架,对 Phi-4 模型进行 Lora 微调,并将其应用于命名实体识别(NER)任务,同时使用 SwanLab 监控训练过程与评估模型效果。
代码:文本的完整微调代码部分,或本目录下的05-Phi-4-Lora-Ner.py
可视化训练过程:ZeyiLin/Phi-4-Lora-Ner
模型:Phi-4
数据集:few_shot_ner_sft
显存需求:约33GB,如显存不足,请调低per_device_train_batch_size
目录
知识点:什么是命名实体识别?
SwanLab简介
1. 环境配置
2. 准备数据集
3. 加载模型
4. 配置LoRA
5. 配置SwanLab可视化工具
6. 完整微调代码
7. 训练结果演示
8. 推理训练好的模型
9. 相关链接
知识点:什么是命名实体识别?
命名实体识别 (NER) 是一种NLP技术,主要用于识别和分类文本中提到的重要信息(关键词)。这些实体可以是人名、地名、机构名、日期、时间、货币值等等。 NER 的目标是将文本中的非结构化信息转换为结构化信息,以便计算机能够更容易地理解和处理。
NER 也是一项非常实用的技术,包括在互联网数据标注、搜索引擎、推荐系统、知识图谱、医疗保健等诸多领域有广泛应用。
SwanLab简介
SwanLab 是一个开源的模型训练记录工具,常被称为"中国版 Weights&Biases + Tensorboard"。SwanLab面向AI研究者,提供了训练可视化、自动日志记录、超参数记录、实验对比、多人协同等功能。在SwanLab上,研究者能基于直观的可视化图表发现训练问题,对比多个实验找到研究灵感,并通过在线链接的分享与基于组织的多人协同训练,打破团队沟通的壁垒。
官网: https://swanlab.cn/
Github: https://github.com/swanhubx/swanlab
为什么要记录训练?
相较于软件开发,模型训练更像一个实验科学。一个品质优秀的模型背后,往往是成千上万次实验。研究者需要不断尝试、记录、对比,积累经验,才能找到最佳的模型结构、超参数与数据配比。在这之中,如何高效进行记录与对比,对于研究效率的提升至关重要。
可视化的价值在哪里?
机器学习模型训练往往伴随着大量的超参数、指标、日志等数据,很多关键信息往往存在于实验的中间而非结尾,如果不对连续的指标通过图表进行可视化,往往会错失发现问题的最佳时机,甚至错过关键信息。同时不进行可视化,也难以对比多个实验之间的差异。 可视化也为AI研究者提供了良好的交流基础,研究者们可以基于图表进行沟通、分析与优化,而非以往看着枯燥的终端打印。这打破了团队沟通的壁垒,提高了整体的研发效率。
- 环境配置
本文基础环境如下:
----------------
ubuntu 22.04
Python 3.12.3
cuda 12.1
pytorch 2.3.0
----------------
本文默认学习者已安装好以上 Pytorch(cuda) 环境,如未安装请自行安装。
首先 pip 换源加速下载并安装依赖包:
# 升级pip
python -m pip install --upgrade pip
# 更换 pypi 源加速库的安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install modelscope==1.22.2 # 用于模型下载和管理
pip install transformers==4.48.0 # Hugging Face 的模型库,用于加载和训练模型
pip install sentencepiece==0.2.0
pip install accelerate==1.3.0 # 用于分布式训练和混合精度训练
pip install datasets==3.2.0 # 用于加载和处理数据集
pip install peft==0.14.0 # 用于 LoRA 微调
pip install swanlab==0.4.3 # 用于监控训练过程与评估模型效果
- 准备数据集
few_shot_ner_sft由不同来源、不同类型的几十万条数据组成,应该是我见过收录最齐全的中文NER数据集。
这次训练我们不需要用到它的全部数据,只取其中的cmeee数据集的前5000条进行训练,该数据集主要被用于医学实体识别任务,包含dis(疾病)、sym(临床表现)、pro(医疗程序)、equ(医疗设备)、dru(药物)、ite(医学检测项目)、bod(身体)、dep(科室)和mic(微生物类)这九种实体类型标注,每条数据的例子如下:
{
"text":
"(5)房室结消融和起搏器植入作为反复发作或难治性心房内折返性心动过速的替代疗法。",
"entities": [
{"start_idx": 3, "end_idx": 6, "entity_text": "房室结消融", "entity_label": "pro"},
{"start_idx": 9, "end_idx": 12, "entity_text": "起搏器植入", "entity_label": "pro"},
{"start_idx": 16, "end_idx": 32, "entity_text": "反复发作或难治性心房内折返性心动过速", "entity_label": "dis"}, {"start_idx": 35, "end_idx": 37, "entity_text": "替代疗法", "entity_label": "pro"}],
"data_source": "cmeee",
"split": "train"
}
其中text是输入的文本,entities是文本抽取出的实体。我们的目标是希望微调后的大模型能够根据由text组成的提示词,预测出一个json格式的实体信息:
输入:非持续性室上性心动过速,不需其他治疗和(或)症状轻微。
大模型输出:{"entity_text":"非持续性室上性心动过速", "entity_label":"dis"}
- 加载模型
这里我们使用modelscope下载Phi-4模型(modelscope在国内,所以直接用下面的代码自动下载即可,不用担心速度和稳定性问题),然后把它加载到Transformers中进行训练:
from modelscope import snapshot_download, AutoTokenizer
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForSeq2Seq
import torch
model_id = "LLM-Research/phi-4"
model_dir = "/root/autodl-tmp/LLM-Research/phi-4/"
# 在modelscope上下载GLM4模型到本地目录下
model_dir = snapshot_download(model_id, cache_dir="/root/autodl-tmp/", revision="master")
# Transformers加载模型权重
tokenizer = AutoTokenizer.from_pretrained(model_dir, use_fast=False, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_dir, device_map="auto", torch_dtype=torch.bfloat16, trust_remote_code=True)
model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法
- 配置LoRA
from peft import LoraConfig, TaskType, get_peft_model
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
inference_mode=False, # 训练模式
r=64, # Lora 秩
lora_alpha=16, # Lora alaph,具体作用参见 Lora 原理
lora_dropout=0.1, # Dropout 比例
)
model = get_peft_model(model, config)
- 配置SwanLab可视化工具
SwanLab与Transformers已经做好了集成,用法是在Trainer的callbacks参数中添加SwanLabCallback实例,就可以自动记录超参数和训练指标,简化代码如下:
from swanlab.integration.transformers import SwanLabCallback
from transformers import Trainer
swanlab_callback = SwanLabCallback()
trainer = Trainer(
...
callbacks=[swanlab_callback],
)
- 完整微调代码
开始训练时的目录结构:
|--- train.py
|--- cmeee.jsonl
下面是train.py的完整代码,直接复制粘贴,然后运行python train.py:
import json
import pandas as pd
import torch
from datasets import Dataset
from modelscope import snapshot_download, AutoTokenizer
from swanlab.integration.huggingface import SwanLabCallback
from peft import LoraConfig, TaskType, get_peft_model
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForSeq2Seq
import os
import swanlab
def dataset_jsonl_transfer(origin_path, new_path):
"""
将原始数据集转换为大模型微调所需数据格式的新数据集
"""
messages = []
# 读取旧的JSONL文件
with open(origin_path, "r") as file:
for line in file:
# 解析每一行的json数据
data = json.loads(line)
input_text = data["text"]
entities = data["entities"]
entity_sentence = ""
for entity in entities:
entity_json = dict(entity)
entity_text = entity_json["entity_text"]
entity_label = entity_json["entity_label"]
entity_sentence += f"""{{"entity_text": "{entity_text}", "entity_label": "{entity_label}"}}"""
if entity_sentence == "":
entity_sentence = "没有找到任何实体"
message = {
"instruction": """
你是一个文本实体识别领域的专家,你需要从给定的句子中提取
- mic
- dru
- pro
- ite
- dis
- sym
- equ
- bod
- dep
这些实体. 以 json 格式输出, 如 {"entity_text": "房室结消融", "entity_label": "procedure"}
注意:
1. 输出的每一行都必须是正确的 json 字符串.
2. 找不到任何实体时, 输出"没有找到任何实体".
""",
"input": f"{input_text}",
"output": entity_sentence,
}
messages.append(message)
# 保存重构后的JSONL文件
with open(new_path, "w", encoding="utf-8") as file:
for message in messages:
file.write(json.dumps(message, ensure_ascii=False) + "\n")
def process_func(example):
"""
将数据集进行预处理
"""
MAX_LENGTH = 384
input_ids, attention_mask, labels = [], [], []
instruction = tokenizer(
f"<|im_start|><im_sep>{example['instruction']}<|im_end|><|im_start|>user<im_sep>{example['input']}<|im_end|><|im_start|>assistant<im_sep>",
add_special_tokens=False,
)
response = tokenizer(f"{example['output']}", add_special_tokens=False)
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
attention_mask = (
instruction["attention_mask"] + response["attention_mask"] + [1]
)
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
if len(input_ids) > MAX_LENGTH: # 做一个截断
input_ids = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels}
def predict(messages, model, tokenizer):
device = "cuda"
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
model_inputs = tokenizer([text], return_tensors="pt").to(device)
generated_ids = model.generate(
model_inputs.input_ids,
max_new_tokens=512
)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)
return response
model_id = "LLM-Research/phi-4"
model_dir = "/root/autodl-tmp/LLM-Research/phi-4"
# 在modelscope上下载Phi-4模型到本地目录下
model_dir = snapshot_download(model_id, cache_dir="/root/autodl-tmp/", revision="master")
# Transformers加载模型权重
tokenizer = AutoTokenizer.from_pretrained(model_dir, use_fast=False, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_dir, device_map="auto", torch_dtype=torch.bfloat16)
model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法
# 加载、处理数据集和测试集
train_dataset_path = "cmeee.jsonl"
train_jsonl_new_path = "cmeee_train.jsonl"
if not os.path.exists(train_jsonl_new_path):
dataset_jsonl_transfer(train_dataset_path, train_jsonl_new_path)
# 得到训练集
total_df = pd.read_json(train_jsonl_new_path, lines=True)[:2000] # 只取2000条数据
train_df = total_df[int(len(total_df) * 0.1):]
train_ds = Dataset.from_pandas(train_df)
train_dataset = train_ds.map(process_func, remove_columns=train_ds.column_names)
lora_rank = 64
lora_alpha = 16
lora_dropout = 0.1
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
inference_mode=False, # 训练模式
r=lora_rank, # Lora 秩
lora_alpha=lora_alpha, # Lora alaph,具体作用参见 Lora 原理
lora_dropout=lora_dropout, # Dropout 比例
)
model = get_peft_model(model, config)
args = TrainingArguments(
output_dir="./output/Phi4-NER",
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
gradient_accumulation_steps=4,
logging_steps=5,
num_train_epochs=1,
save_steps=100,
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True,
report_to="none",
)
swanlab_callback = SwanLabCallback(
project="Phi-4-NER-fintune",
experiment_name="phi-4",
description="使用Phi-4模型在qgyd2021/few_shot_ner_sft - cmeee.jsonl数据集上的前5000条数据进行微调,实现关键实体识别任务(医疗领域)。",
config={
"model": model_id,
"model_dir": model_dir,
"dataset": "https://huggingface.co/datasets/qgyd2021/few_shot_ner_sft",
"sub_dataset": "cmeee.jsonl",
"lora_rank": lora_rank,
"lora_alpha": lora_alpha,
"lora_dropout": lora_dropout,
},
)
trainer = Trainer(
model=model,
args=args,
train_dataset=train_dataset,
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
callbacks=[swanlab_callback],
)
trainer.train()
# 用测试集的随机20条,测试模型
# 得到测试集
test_df = total_df[:int(len(total_df) * 0.1)].sample(n=5)
test_text_list = []
for index, row in test_df.iterrows():
instruction = row['instruction']
input_value = row['input']
messages = [
{"role": "system", "content": f"{instruction}"},
{"role": "user", "content": f"{input_value}"}
]
response = predict(messages, model, tokenizer)
result_text = f"用户输入: {input_value} | 大模型输出: {response}"
test_text_list.append(swanlab.Text(result_text))
swanlab.log({"Prediction": test_text_list})
swanlab.finish()
看到下面的进度条即代表训练开始:
- 训练结果演示
在SwanLab上查看最终的训练结果:
可以看到在1个epoch之后,微调后的Phi-4的loss降低到了不错的水平。
可以看到在一些测试样例上,微调后的Phi-4能够给出准确的NER结果:
至此,你已经完成了Phi-4 Lora微调的训练!如果需要加强微调效果,可以尝试增加训练的数据量。
- 推理训练好的模型
训好的Lora模型默认被保存在./output/Phi4-NER/目录下,你可以使用下面的代码进行推理:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
def predict(messages, model, tokenizer):
device = "cuda"
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
model_inputs = tokenizer([text], return_tensors="pt").to(device)
generated_ids = model.generate(model_inputs.input_ids, max_new_tokens=512)
generated_ids = [output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
return response
model_dir = "/root/autodl-tmp/LLM-Research/phi-4"
lora_dir = "./output/Phi4-NER/checkpoint-112"
# 加载原下载路径的tokenizer和model
tokenizer = AutoTokenizer.from_pretrained(model_dir, use_fast=False, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_dir, device_map="auto", torch_dtype=torch.bfloat16)
# 加载训练好的Lora模型
model = PeftModel.from_pretrained(model, model_id=lora_dir)
input_text = "肾静态显像观察到急性肾盂肾炎患儿肾瘢痕的阳性率达50%左右,瘢痕征的表现为肾影中单个或多个局部放射性缺损或减低区,多位于上下极,典型者呈楔形,宽面向外,使整个肾影变形。"
test_texts = {
"instruction": """
你是一个文本实体识别领域的专家,你需要从给定的句子中提取
- mic
- dru
- pro
- ite
- dis
- sym
- equ
- bod
- dep
这些实体. 以 json 格式输出, 如 {"entity_text": "房室结消融", "entity_label": "procedure"}
注意:
1. 输出的每一行都必须是正确的 json 字符串.
2. 找不到任何实体时, 输出"没有找到任何实体".
""",
"input": f"{input_text}"
}
instruction = test_texts['instruction']
input_value = test_texts['input']
messages = [
{"role": "system", "content": f"{instruction}"},
{"role": "user", "content": f"{input_value}"}
]
response = predict(messages, model, tokenizer)
print(response)
输出结果为:
{"entity_text": "肾静态显像", "entity_label": "pro"}
{"entity_text": "急性肾盂肾炎", "entity_label": "dis"}
{"entity_text": "肾瘢痕", "entity_label": "sym"}
{"entity_text": "肾影中单个或多个局部放射性缺损或减低区", "entity_label": "sym"}
{"entity_text": "肾影", "entity_label": "sym"}
{"entity_text": "肾", "entity_label": "bod"}
- 相关链接
代码:文本的完整微调代码部分,或本目录下的05-Phi-4-Lora-Ner.py
SwanLab:官网、Github
可视化训练过程:ZeyiLin/Phi-4-Lora-Ner
模型:Phi-4
数据集:few_shot_ner_sft
显存需求:约33GB,如显存不足,请调低per_device_train_batch_size
Gemma3-4B GRPO微调教程
话不多说,直接开始!
本文使用的测试环境为单张 A100,显存 80GB,可根据需求切换不同参数量的模型,实测4B 24G显存 is enough! 使用的框架为 Unsloth
Unsloth 是一个极其强调资源节省的框架,把所有的资源节省做到了极致,具体来讲Unsloth能够将 Llama-3、Mistral、Phi-4 和 Gemma 等大型语言模型的微调速度提升 2 倍,内存占用减少 70%,并且准确率没有任何下降! 官方文档非常全面,详细指导了如何训练自己的定制模型。其中涵盖了安装和更新 Unsloth、创建数据集、运行和部署模型等基本要素。 Unsloth 让大家在本地或在 Google Colab 和 Kaggle 等平台上训练像 Llama 3 这样的模型变得极其简单。Unsloth简化了整个训练工作流程,包括模型加载、量化、训练、评估、运行、保存、导出,以及与 Ollama、llama.cpp 和 vLLM 等推理引擎的集成。 Unsloth定期与 Hugging Face、Google 和 Meta 的团队合作,以修复 LLM 训练和模型中的错误。因此,当使用 Unsloth 进行训练或使用模型时,可以期待获得最准确的结果。 Unsloth 具有高度可定制性,允许更改聊天模板或数据集格式等内容。Unsloth还为视觉、文本转语音 (TTS)、BERT、强化学习 (RL) 等提供了预构建的脚本!此外,Unsloth支持所有训练方法和所有基于 Transformer 的模型。
unsloth使Phi-4微调速度提高2倍,VRAM使用减少70%,并且比所有使用Flash Attention 2的环境支持长8倍的上下文长度。使用unsloth,Phi-4模型可以舒适地在仅24GB VRAM的环境中运行。 unsloth为Phi-4提供了Dynamic 2.0量化方法,在5-shot MMLU和KL散度基准测试中提供最佳性能。这意味着可以运行和微调量化后的Phi-4 LLM,同时保持最小的精度损失。 unsloth还上传了支持原生长上下文的Phi-4版本。
教程概览
本教程将指导您完成 Phi-4(14B) 模型的 GRPO(Group Relative Policy Optimization)微调,这是一种先进的强化学习技术,专门用于提升大语言模型在特定任务上的表现。
什么是GRPO?
GRPO(Group Relative Policy Optimization)是一种强化学习优化技术,通过设计多个奖励函数来评估模型输出的不同方面,从而指导模型学习期望的行为模式。在数学推理任务中,GRPO可以帮助模型:
学会按照特定格式输出答案
提高推理过程的逻辑性
增强答案的准确性
改善输出的结构化程度
本教程的学习内容
环境设置: 安装Unsloth和相关依赖
模型加载: 加载Phi-4(14B)预训练模型
LoRA配置: 设置高效的参数微调
数据处理: 处理GSM8K数学推理数据集
格式设计: 定义结构化的输出格式
奖励函数: 设计多维度评估体系
GRPO训练: 执行强化学习微调
效果验证: 测试微调后的模型
模型保存: 保存训练结果
可视化监控: 使用SwanLab跟踪训练过程
概述
本教程将展示如何使用 GRPO (Generalized Reward-based Policy Optimization) 算法对 Phi-4 模型进行强化学习训练,并使用 SwanLab 进行训练过程的可视化监控。
GRPO 是一种基于奖励的策略优化方法,能够帮助模型学习更好的推理能力和格式化输出。
第一步:环境准备和依赖安装
首先需要安装必要的依赖包,包括 Unsloth 和 vLLM。
# 安装必要的依赖包
# unsloth: 用于快速模型训练和推理的优化库
# vllm: 高性能的大语言模型推理引擎
# pip install unsloth vllm
第二步:模型加载和 LoRA 配置
在这一步中,我们将:
加载预训练的 Phi-4 模型
配置 LoRA (Low-Rank Adaptation) 参数用于高效微调
设置模型的基本训练参数
LoRA 是一种参数高效的微调方法,只训练少量参数就能获得良好的性能。
# 导入必要的库
from unsloth import FastLanguageModel, is_bfloat16_supported
import torch
# 模型配置参数
max_seq_length = 512 # 最大序列长度,可以增加以支持更长的推理链
lora_rank = 16 # LoRA 秩,更大的秩会让模型更聪明但训练更慢
# 加载预训练模型和分词器
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/Phi-4", # Phi-4 模型路径,使用量化模型以节省内存
max_seq_length = max_seq_length, # 设置最大序列长度
load_in_4bit = True, # 启用 4bit 量化以减少内存使用,设为 False 则使用 16bit LoRA
fast_inference = True, # 启用 vLLM 快速推理引擎
max_lora_rank = lora_rank, # 设置最大 LoRA 秩
gpu_memory_utilization = 0.7, # GPU 内存使用率,如果内存不足可以减少
)
# 配置 PEFT (Parameter-Efficient Fine-Tuning) 模型
model = FastLanguageModel.get_peft_model(
model,
r = lora_rank, # LoRA 秩,建议值:8, 16, 32, 64, 128
target_modules = ["gate_proj", "up_proj", "down_proj"], # 目标模块,这些是 MLP 层的关键组件
lora_alpha = lora_rank, # LoRA alpha 参数,通常设为与秩相同的值
use_gradient_checkpointing = "unsloth", # 启用梯度检查点以支持长上下文微调
random_state = 3407, # 随机种子,确保结果可复现
)
🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning. 🦥 Unsloth Zoo will now patch everything to make training faster! INFO 07-02 17:05:40 [init.py:239] Automatically detected platform cuda. ==((====))== Unsloth 2025.6.12: Fast Llama patching. Transformers: 4.52.4. vLLM: 0.8.2. \ /| NVIDIA A100-SXM4-80GB. Num GPUs = 1. Max memory: 79.151 GB. Platform: Linux. O^O/ _/ \ Torch: 2.6.0+cu124. CUDA: 8.0. CUDA Toolkit: 12.4. Triton: 3.2.0 \ / Bfloat16 = TRUE. FA [Xformers = 0.0.29.post2. FA2 = True] "-____-" Free license: http://github.com/unslothai/unsloth Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored! Unsloth: vLLM loading /opt/tiger/test0/Phi-4 with actual GPU utilization = 69.6% Unsloth: Your GPU has CUDA compute capability 8.0 with VRAM = 79.15 GB. Unsloth: Using conservativeness = 1.0. Chunked prefill tokens = 512. Num Sequences = 320. Unsloth: vLLM's KV Cache can use up to 27.66 GB. Also swap space = 6 GB. INFO 07-02 17:07:52 [config.py:585] This model supports multiple tasks: {'classify', 'generate', 'embed', 'score', 'reward'}. Defaulting to 'generate'. WARNING 07-02 17:07:52 [arg_utils.py:1854] --quantization bitsandbytes is not supported by the V1 Engine. Falling back to V0. Unsloth: vLLM Bitsandbytes config using kwargs = {'load_in_8bit': False, 'load_in_4bit': True, 'bnb_4bit_compute_dtype': 'bfloat16', 'bnb_4bit_quant_storage': 'uint8', 'bnb_4bit_quant_type': 'fp4', 'bnb_4bit_use_double_quant': False, 'llm_int8_enable_fp32_cpu_offload': False, 'llm_int8_has_fp16_weight': False, 'llm_int8_skip_modules': [], 'llm_int8_threshold': 6.0} INFO 07-02 17:07:52 [llm_engine.py:241] Initializing a V0 LLM engine (v0.8.2) with config: model='/opt/tiger/test0/Phi-4', speculative_config=None, tokenizer='/opt/tiger/test0/Phi-4', skip_tokenizer_init=False, tokenizer_mode=auto, revision=None, override_neuron_config=None, tokenizer_revision=None, trust_remote_code=False, dtype=torch.bfloat16, max_seq_len=512, download_dir=None, load_format=LoadFormat.BITSANDBYTES, tensor_parallel_size=1, pipeline_parallel_size=1, disable_custom_all_reduce=False, quantization=bitsandbytes, enforce_eager=False, kv_cache_dtype=auto, device_config=cuda:0, decoding_config=DecodingConfig(guided_decoding_backend='xgrammar', reasoning_backend=None), observability_config=ObservabilityConfig(show_hidden_metrics=False, otlp_traces_endpoint=None, collect_model_forward_time=False, collect_model_execute_time=False), seed=0, served_model_name=/opt/tiger/test0/Phi-4, num_scheduler_steps=1, multi_step_stream_outputs=True, enable_prefix_caching=True, chunked_prefill_enabled=False, use_async_output_proc=True, disable_mm_preprocessor_cache=False, mm_processor_kwargs=None, pooler_config=None, compilation_config={"level":0,"backend":"inductor","splitting_ops":[],"use_inductor":true,"compile_sizes":[],"inductor_compile_config":{"debug":false,"dce":true,"coordinate_descent_tuning":true,"trace.enabled":false,"trace.graph_diagram":false,"triton.cudagraphs":true,"compile_threads":48,"max_autotune":false,"disable_progress":false,"verbose_progress":true,"enable_auto_functionalized_v2":false},"use_cudagraph":true,"cudagraph_num_of_warmups":1,"cudagraph_capture_sizes":[320,312,304,296,288,280,272,264,256,248,240,232,224,216,208,200,192,184,176,168,160,152,144,136,128,120,112,104,96,88,80,72,64,56,48,40,32,24,16,8,4,2,1],"max_capture_size":320}, use_cached_outputs=False, INFO 07-02 17:07:53 [cuda.py:291] Using Flash Attention backend. INFO 07-02 17:07:53 [parallel_state.py:954] rank 0 in world size 1 is assigned as DP rank 0, PP rank 0, TP rank 0 INFO 07-02 17:07:53 [model_runner.py:1110] Starting to load model /opt/tiger/test0/Phi-4... INFO 07-02 17:07:53 [loader.py:1155] Loading weights with BitsAndBytes quantization. May take a while ...
Loading safetensors checkpoint shards: 0% Completed | 0/6 [00:00<?, ?it/s]
INFO 07-02 17:08:01 [punica_selector.py:18] Using PunicaWrapperGPU. INFO 07-02 17:08:01 [model_runner.py:1146] Model loading took 8.6253 GB and 7.670299 seconds INFO 07-02 17:08:07 [worker.py:267] Memory profiling takes 5.35 seconds INFO 07-02 17:08:07 [worker.py:267] the current vLLM instance can use total_gpu_memory (79.15GiB) x gpu_memory_utilization (0.70) = 55.09GiB INFO 07-02 17:08:07 [worker.py:267] model weights take 8.63GiB; non_torch_memory takes 0.09GiB; PyTorch activation peak memory takes 1.16GiB; the rest of the memory reserved for KV Cache is 45.21GiB. INFO 07-02 17:08:07 [executor_base.py:111] # cuda blocks: 14815, # CPU blocks: 1966 INFO 07-02 17:08:07 [executor_base.py:116] Maximum concurrency for 512 tokens per request: 462.97x INFO 07-02 17:08:12 [model_runner.py:1442] Capturing cudagraphs for decoding. This may lead to unexpected consequences if the model is not static. To run the model in eager mode, set 'enforce_eager=True' or use '--enforce-eager' in the CLI. If out-of-memory error occurs during cudagraph capture, consider decreasing gpu_memory_utilization or switching to eager mode. You can also reduce the max_num_seqs as needed to decrease memory usage.
Capturing CUDA graph shapes: 100%|██████████| 43/43 [00:58<00:00, 1.37s/it]
INFO 07-02 17:09:11 [model_runner.py:1570] Graph capturing finished in 59 secs, took 1.12 GiB INFO 07-02 17:09:11 [llm_engine.py:447] init engine (profile, create kv cache, warmup model) took 70.01 seconds
Unsloth: Just some info: will skip parsing ['pre_feedforward_layernorm', 'k_norm', 'q_norm', 'post_feedforward_layernorm'] Unsloth: Just some info: will skip parsing ['pre_feedforward_layernorm', 'k_norm', 'q_norm', 'post_feedforward_layernorm']
Not an error, but Unsloth cannot patch Attention layers with our manual autograd engine since either LoRA adapters are not enabled or a bias term (like in Qwen) is used. Not an error, but Unsloth cannot patch O projection layer with our manual autograd engine since either LoRA adapters are not enabled or a bias term (like in Qwen) is used. Unsloth 2025.6.12 patched 40 layers with 0 QKV layers, 0 O layers and 40 MLP layers.
第三步:数据集准备和奖励函数定义
在这一步中,我们将:
加载和预处理 GSM8K 数学问题数据集
定义系统提示词和输出格式
创建多个奖励函数来指导模型学习
奖励函数是 GRPO 训练的核心,它们会评估模型输出的质量并给出反馈。
import re
from datasets import load_dataset, Dataset
# Load and prep dataset
SYSTEM_PROMPT = """
Respond in the following format:
<reasoning>
...
</reasoning>
<answer>
...
</answer>
"""
XML_COT_FORMAT = """\
<reasoning>
{reasoning}
</reasoning>
<answer>
{answer}
</answer>
"""
def extract_xml_answer(text: str) -> str:
answer = text.split("<answer>")[-1]
answer = answer.split("</answer>")[0]
return answer.strip()
def extract_hash_answer(text: str) -> str | None:
if "####" not in text:
return None
return text.split("####")[1].strip()
# uncomment middle messages for 1-shot prompting
def get_gsm8k_questions(split = "train") -> Dataset:
data = load_dataset('openai/gsm8k', 'main')[split] # type: ignore
data = data.map(lambda x: { # type: ignore
'prompt': [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': x['question']}
],
'answer': extract_hash_answer(x['answer'])
}) # type: ignore
return data # type: ignore
dataset = get_gsm8k_questions()
# Reward functions
def correctness_reward_func(prompts, completions, answer, **kwargs) -> list[float]:
responses = [completion[0]['content'] for completion in completions]
q = prompts[0][-1]['content']
extracted_responses = [extract_xml_answer(r) for r in responses]
print('-'*20, f"Question:\n{q}", f"\nAnswer:\n{answer[0]}", f"\nResponse:\n{responses[0]}", f"\nExtracted:\n{extracted_responses[0]}")
return [2.0 if r == a else 0.0 for r, a in zip(extracted_responses, answer)]
def int_reward_func(completions, **kwargs) -> list[float]:
responses = [completion[0]['content'] for completion in completions]
extracted_responses = [extract_xml_answer(r) for r in responses]
return [0.5 if r.isdigit() else 0.0 for r in extracted_responses]
def strict_format_reward_func(completions, **kwargs) -> list[float]:
"""Reward function that checks if the completion has a specific format."""
pattern = r"^<reasoning>\n.*?\n</reasoning>\n<answer>\n.*?\n</answer>\n$"
responses = [completion[0]["content"] for completion in completions]
matches = [re.match(pattern, r) for r in responses]
return [0.5 if match else 0.0 for match in matches]
def soft_format_reward_func(completions, **kwargs) -> list[float]:
"""Reward function that checks if the completion has a specific format."""
pattern = r"<reasoning>.*?</reasoning>\s*<answer>.*?</answer>"
responses = [completion[0]["content"] for completion in completions]
matches = [re.match(pattern, r) for r in responses]
return [0.5 if match else 0.0 for match in matches]
def count_xml(text) -> float:
count = 0.0
if text.count("<reasoning>\n") == 1:
count += 0.125
if text.count("\n</reasoning>\n") == 1:
count += 0.125
if text.count("\n<answer>\n") == 1:
count += 0.125
count -= len(text.split("\n</answer>\n")[-1])*0.001
if text.count("\n</answer>") == 1:
count += 0.125
count -= (len(text.split("\n</answer>")[-1]) - 1)*0.001
return count
def xmlcount_reward_func(completions, **kwargs) -> list[float]:
contents = [completion[0]["content"] for completion in completions]
return [count_xml(c) for c in contents]
from trl import GRPOConfig, GRPOTrainer
training_args = GRPOConfig(
use_vllm = True, # use vLLM for fast inference!
learning_rate = 5e-6,
adam_beta1 = 0.9,
adam_beta2 = 0.99,
weight_decay = 0.1,
warmup_ratio = 0.1,
lr_scheduler_type = "cosine",
optim = "paged_adamw_8bit",
logging_steps = 1,
per_device_train_batch_size = 1,
gradient_accumulation_steps = 1, # Increase to 4 for smoother training
num_generations = 6, # Decrease if out of memory
max_prompt_length = 256,
max_completion_length = 200,
# num_train_epochs = 1, # Set to 1 for a full training run
max_steps = 100,
save_steps = 250,
max_grad_norm = 0.1,
report_to = "swanlab", # Can use Weights & Biases
output_dir = "outputs",
)
Unsloth: We now expect per_device_train_batch_size to be a multiple of num_generations. We will change the batch size of 1 to the num_generations of 6
And let's run the trainer! If you scroll up, you'll see a table of rewards. The goal is to see the reward column increase!
You might have to wait 150 to 200 steps for any action. You'll probably get 0 reward for the first 100 steps. Please be patient!
| Step | Training Loss | reward | reward_std | completion_length | kl |
| 1 | 0.000000 | 0.125000 | 0.000000 | 200.000000 | 0.000000 |
| 2 | 0.000000 | 0.072375 | 0.248112 | 200.000000 | 0.000000 |
| 3 | 0.000000 | -0.079000 | 0.163776 | 182.500000 | 0.000005 |
trainer = GRPOTrainer(
model = model,
processing_class = tokenizer,
reward_funcs = [
xmlcount_reward_func,
soft_format_reward_func,
strict_format_reward_func,
int_reward_func,
correctness_reward_func,
],
args = training_args,
train_dataset = dataset,
)
trainer.train()
Detected kernel version 5.4.143, which is below the recommended minimum of 5.5.0; this can cause the process to hang. It is recommended to upgrade the kernel to the minimum version or higher. ==((====))== Unsloth - 2x faster free finetuning | Num GPUs used = 1 \ /| Num examples = 7,473 | Num Epochs = 1 | Total steps = 100 O^O/ _/ \ Batch size per device = 6 | Gradient accumulation steps = 1 \ / Data Parallel GPUs = 1 | Total batch size (6 x 1 x 1) = 6 "-____-" Trainable parameters = 44,236,800 of 7,888,000,000 (0.56% trained)
[1m[34mswanlab[0m[0m: Tracking run with swanlab version 0.6.4 [1m[34mswanlab[0m[0m: Run data will be saved locally in [35m[1m/opt/tiger/test0/swanlog/run-20250702_170943-0e8cd89d[0m[0m [1m[34mswanlab[0m[0m: 👋 Hi [1m[39mtwosugar[0m[0m, welcome to swanlab! [1m[34mswanlab[0m[0m: Syncing run [33moutputs[0m to the cloud [1m[34mswanlab[0m[0m: 🏠 View project at [34m[4mhttps://swanlab.cn/@twosugar/test0[0m[0m [1m[34mswanlab[0m[0m: 🚀 View run at [34m[4mhttps://swanlab.cn/@twosugar/test0/runs/rnvceekqy652of27de2o9[0m[0m
text = tokenizer.apply_chat_template([
{"role" : "user", "content" : "Which is bigger? 9.11 or 9.9?"},
], tokenize = False, add_generation_prompt = True)
from vllm import SamplingParams
sampling_params = SamplingParams(
temperature = 0.8,
top_p = 0.95,
max_tokens = 1024,
)
output = model.fast_generate(
[text],
sampling_params = sampling_params,
lora_request = None,
)[0].outputs[0].text
output
Processed prompts: 100%|██████████| 1/1 [00:16<00:00, 16.59s/it, est. speed input: 1.27 toks/s, output: 9.89 toks/s]
'9.11 is bigger than 9.9. When comparing decimal numbers, you look at the digits from left to right. Both numbers have the same whole number part (9), so you compare the digits in the tenths place next. In 9.11, the tenths place is 1, and in 9.9, the tenths place is 9. Since 1 is less than 9, you might initially think 9.9 is larger, but you also need to consider the hundredths place in 9.11, which is 1. When you express 9.9 as 9.90 for comparison, you see that 9.11 is greater than 9.90. Therefore, 9.11 is bigger than 9.9.'
model.save_lora("grpo_saved_lora")
text = tokenizer.apply_chat_template([
{"role" : "system", "content" : SYSTEM_PROMPT},
{"role" : "user", "content" : "Which is bigger? 9.11 or 9.9?"},
], tokenize = False, add_generation_prompt = True)
from vllm import SamplingParams
sampling_params = SamplingParams(
temperature = 0.8,
top_p = 0.95,
max_tokens = 1024,
)
output = model.fast_generate(
text,
sampling_params = sampling_params,
lora_request = model.load_lora("grpo_saved_lora"),
)[0].outputs[0].text
output
Processed prompts: 100%|██████████| 1/1 [00:27<00:00, 27.72s/it, est. speed input: 1.70 toks/s, output: 10.03 toks/s]
'\nTo determine which number is bigger between 9.11 and 9.9, we should compare the two numbers digit by digit from left to right. \n\n1. First, compare the digits in the units place:\n - Both numbers have a 9 in the units place.\n\n2. Next, compare the digits in the tenths place:\n - The number 9.11 has a 1 in the tenths place.\n - The number 9.9 has a 9 in the tenths place.\n\nSince 1 is less than 9, the number 9.11 is less than 9.9 based on the tenths place comparison.\n\n3. For thoroughness, consider the hundredths place:\n - The number 9.11 has a 1 in the hundredths place.\n - The number 9.9 can be written as 9.90, which has a 0 in the hundredths place.\n\nEven if we compare the hundredths place, 1 is greater than 0, but this is irrelevant since the comparison in the tenths place already determines that 9.11 is smaller than 9.9.\n\nTherefore, 9.9 is greater than 9.11.\n\n\n\n9.9 is bigger than 9.11.\n'
print(output)
To determine which number is bigger between 9.11 and 9.9, we should compare the two numbers digit by digit from left to right.
First, compare the digits in the units place:
Both numbers have a 9 in the units place.
Next, compare the digits in the tenths place:
The number 9.11 has a 1 in the tenths place.
The number 9.9 has a 9 in the tenths place.
Since 1 is less than 9, the number 9.11 is less than 9.9 based on the tenths place comparison.
For thoroughness, consider the hundredths place:
The number 9.11 has a 1 in the hundredths place.
The number 9.9 can be written as 9.90, which has a 0 in the hundredths place.
Even if we compare the hundredths place, 1 is greater than 0, but this is irrelevant since the comparison in the tenths place already determines that 9.11 is smaller than 9.9.
Therefore, 9.9 is greater than 9.11.
9.9 is bigger than 9.11.
Our reasoning model is much better - it's not always correct, since we only trained it for an hour or so - it'll be better if we extend the sequence length and train for longer!
# Merge to 16bit
if False: model.save_pretrained_merged("model", tokenizer, save_method = "merged_16bit",)
if False: model.push_to_hub_merged("hf/model", tokenizer, save_method = "merged_16bit", token = "")
# Merge to 4bit
if False: model.save_pretrained_merged("model", tokenizer, save_method = "merged_4bit",)
if False: model.push_to_hub_merged("hf/model", tokenizer, save_method = "merged_4bit", token = "")
# Just LoRA adapters
if False:
model.save_pretrained("model")
tokenizer.save_pretrained("model")
if False:
model.push_to_hub("hf/model", token = "")
tokenizer.push_to_hub("hf/model", token = "")
# Save to 8bit Q8_0
if False: model.save_pretrained_gguf("model", tokenizer,)
# Remember to go to https://huggingface.co/settings/tokens for a token!
# And change hf to your username!
if False: model.push_to_hub_gguf("hf/model", tokenizer, token = "")
# Save to 16bit GGUF
if False: model.save_pretrained_gguf("model", tokenizer, quantization_method = "f16")
if False: model.push_to_hub_gguf("hf/model", tokenizer, quantization_method = "f16", token = "")
# Save to q4_k_m GGUF
if False: model.save_pretrained_gguf("model", tokenizer, quantization_method = "q4_k_m")
if False: model.push_to_hub_gguf("hf/model", tokenizer, quantization_method = "q4_k_m", token = "")
# Save to multiple GGUF options - much faster if you want multiple!
if False:
model.push_to_hub_gguf(
"hf/model", # Change hf to your username!
tokenizer,
quantization_method = ["q4_k_m", "q8_0", "q5_k_m",],
token = "",
)
Swanlab
++SwanLab++ 是一个开源的模型训练记录工具,面向 AI 研究者,提供了训练可视化、自动日志记录、超参数记录、实验对比、多人协同等功能。在
SwanLab上,研究者能基于直观的可视化图表发现训练问题,对比多个实验找到研究灵感,并通过在线链接的分享与基于组织的多人协同训练,打破团队沟通的壁垒。
为什么要记录训练?
相较于软件开发,模型训练更像一个实验科学。一个品质优秀的模型背后,往往是成千上万次实验。研究者需要不断尝试、记录、对比,积累经验,才能找到最佳的模型结构、超参数与数据配比。在这之中,如何高效进行记录与对比,对于研究效率的提升至关重要。
在哪里用?
from trl import GRPOConfig, GRPOTrainer
training_args = GRPOConfig(
# 优化器参数
learning_rate = 5e-6, # 学习率:GRPO通常使用较小的学习率
adam_beta1 = 0.9, # Adam优化器的beta1参数
adam_beta2 = 0.99, # Adam优化器的beta2参数
weight_decay = 0.1, # 权重衰减,防止过拟合
optim = "adamw_torch_fused", # 使用融合的AdamW优化器,更高效
# 学习率调度
warmup_ratio = 0.1, # 学习率预热比例
lr_scheduler_type = "cosine", # 余弦学习率调度
# 训练批次设置
per_device_train_batch_size = 1, # 每个设备的批次大小
gradient_accumulation_steps = 1, # 梯度累积步数(可以增加到4获得更平滑的训练)
num_generations = 4, # 每个提示生成的候选数量(显存不足时可减少)
# 序列长度控制
max_prompt_length = max_prompt_length, # 提示的最大长度
max_completion_length = max_seq_length - max_prompt_length, # 完成文本的最大长度
# 训练控制
max_steps = 50, # 最大训练步数(演示用,实际训练建议更多)
save_steps = 50, # 保存模型的步数间隔
max_grad_norm = 0.1, # 梯度裁剪阈值
# 日志和监控
logging_steps = 1, # 日志记录间隔
report_to = "swanlab", # 这里改成swanlab
output_dir = "outputs", # 输出目录
)
本试验的试验记录
GRPO阶段
400个step之后loss会有明显变化
教程总结
🎉 恭喜!你已经成功完成了Phi-4(14B)的GRPO微调教程。
本教程涵盖的核心概念:
GRPO微调: 使用奖励函数指导模型学习特定输出格式
LoRA技术: 高效的参数微调方法,节省显存和时间
奖励函数设计: 多层次评估体系,从格式到内容的全面评价
结构化输出: 训练模型按照特定格式输出推理过程和答案
SwanLab监控: 实时跟踪训练进度和指标变化
学到的技能:
✅ 设置GRPO训练环境
✅ 设计多维度奖励函数
✅ 配置LoRA参数进行高效微调
✅ 处理数学推理数据集
✅ 监控和分析训练过程
✅ 保存和部署微调模型
进一步探索:
调整奖励函数: 设计更复杂的评估机制
扩展数据集: 使用更大或不同类型的数据集
优化参数: 尝试不同的LoRA配置和训练参数
模型评估: 在测试集上系统评估模型性能
应用部署: 将模型集成到实际应用中
注意事项:
本教程使用了较少的训练步数作为演示,实际应用中建议使用更多步数
可以根据显存情况调整批次大小和生成数量
SwanLab提供了丰富的可视化功能,建议深入探索
感谢你的学习!如果有任何问题,欢迎查看SwanLab的实验记录或重新运行代码。
Congratulations!看到了这,你已经初步实现了一个简单的RL实战,掌握了使用 Unsloth 对 Phi-4(14B) 这类大模型进行 GRPO 微调的具体操作步骤,更能体会到 Unsloth 在大幅提升训练速度、显著降低显存占用方面的强大优势,从而使在有限资源下进行复杂强化学习实验成为可能!如果支持我们的工作希望得到你的star!!这是我们持续更新的最大动力!!!
完整可运行的代码:Github
综述:https://arxiv.org/abs/2001.06921
deepseek-r1:https://arxiv.org/abs/2501.12948
数学原理:https://blog.csdn.net/weixin_38991876/article/details/146474767
Unsloth:https://docs.unsloth.ai/