一、概述
Qwen3是通义千问团队的开源大语言模型,由阿里云通义实验室研发。以Qwen3作为基座大模型,通过指令微调的方式实现高准确率的文本分类,是学习大语言模型微调的入门任务。
指令微调是一种通过在由(指令,输出)对组成的数据集上进一步训练LLMs的过程。 其中,指令代表模型的人类指令,输出代表遵循指令的期望输出。 这个过程有助于弥合LLMs的下一个词预测目标与用户让LLMs遵循人类指令的目标之间的差距。
在这个任务中我们会使用Qwen3-1.7B模型在zh_cls_fudan_news数据集上进行指令微调任务,同时使用SwanLab进行监控和可视化。
实验日志过程:https://swanlab.cn/@spark_xiao/Qwen3-fintune/runs/4vyja2grzjuzfpu23e94a
参考代码:https://github.com/Zeyi-Lin/LLM-Finetune
模型:https://modelscope.cn/models/Qwen/Qwen3-1.7B
数据集:https://www.modelscope.cn/datasets/swift/zh_cls_fudan-news/summary
SwanLab:https://swanlab.cn
二、SwanLab
SwanLab(https://swanlab.cn)是一个用于AI模型训练过程可视化的工具。SwanLab的主要功能包括:
跟踪模型指标,如损失和准确性等
同时支持云端和离线使用,支持远程查看训练过程,比如可以在手机上看远程服务器上跑的训练
记录训练超参数,如batch_size和learning_rate等
自动记录训练过程中的日志、硬件环境、Python库以及GPU(支持英伟达显卡)、NPU(支持华为昇腾卡)、内存的硬件信息
支持团队多人协作,很适合打Kaggle等比赛的队伍
SwanLab库来自一个中国团队(情感机器),最早的出发点是其开发团队的内部训练需求,后来逐渐开源并且发展成面向公众的产品。SwanLab库在2024年向公众发布。SwanLab刚出现时只有离线版本(对标Tensorboard),后来经过迭代和努力已经有了云端版和各项功能,并且集成了接近30+个深度学习框架,包括PyTorch、HuggingFace Transformers、Keras、XGBoost等等,其中还包括同样是中国团队开发的LLaMA Factory、Modelscope Swift、PaddleYOLO等框架,具有了很全面的功能。
账号注册
SwanLab的云端版体验是比较好的(非常推荐),能够支持你在随时随地访问训练过程。
要使用云端版之前需要先注册一下账号:
在电脑或手机浏览器访问SwanLab官网: https://swanlab.cn
点击右上角注册
填写手机号后,点击「发送短信验证码」按钮
填写你的信息
- 用户名称:你的个人昵称,中英文均可
- 用户ID:你的英文名,可由数字、字母、下划线、中横线组成
- 邮箱:你的邮箱
- 机构/院校:你所在的企业、机构或学校
- 您从哪了解到SwanLab?:(选填项)了解到SwanLab的渠道,比如朋友介绍
复制API Key
完成填写后点击「完成」按钮,会进入到下面的页面。然后点击左边的「设置」:
在API Key这个地方,点击复制按钮,复制你的API Key:
三、环境安装
本案例基于Python 3.13.2,请在您的计算机上安装好Python,并且有一张英伟达显卡(显存要求并不高,大概10GB左右就可以跑)。
在这之前,请确保你的环境内已安装了pytorch以及CUDA:
pytorch以及CUDA安装,请参考文章:https://www.cnblogs.com/xiao987334176/p/18876317
我们需要安装以下这几个Python库,一键安装命令:
pip install swanlab modelscope transformers datasets peft pandas accelerate
准备数据集
本案例使用的是zh_cls_fudan-news数据集,该数据集主要被用于训练文本分类模型。
zh_cls_fudan-news由几千条数据,每条数据包含text、category、output三列:
text 是训练语料,内容是书籍或新闻的文本内容
category 是text的多个备选类型组成的列表
output 则是text唯一真实的类型
数据集例子如下:
""" [PROMPT]Text: 第四届全国大企业足球赛复赛结束新华社郑州5月3日电(实习生田兆运)上海大隆机器厂队昨天在洛阳进行的第四届牡丹杯全国大企业足球赛复赛中,以5:4力克成都冶金实验厂队,进入前四名。沪蓉之战,双方势均力敌,90分钟不分胜负。最后,双方互射点球,沪队才以一球优势取胜。复赛的其它3场比赛,青海山川机床铸造厂队3:0击败东道主洛阳矿山机器厂队,青岛铸造机械厂队3:1战胜石家庄第一印染厂队,武汉肉联厂队1:0险胜天津市第二冶金机械厂队。在今天进行的决定九至十二名的两场比赛中,包钢无缝钢管厂队和河南平顶山矿务局一矿队分别击败河南平顶山锦纶帘子布厂队和江苏盐城无线电总厂队。4日将进行两场半决赛,由青海山川机床铸造厂队和青岛铸造机械厂队分别与武汉肉联厂队和上海大隆机器厂队交锋。本届比赛将于6日结束。(完) Category: Sports, Politics Output:[OUTPUT]Sports """
我们的训练任务,便是希望微调后的大模型能够根据Text和Category组成的提示词,预测出正确的Output。
我们将数据集下载到本地目录下。下载方式是前往zh_cls_fudan-news - 魔搭社区 ,将train.jsonl
和test.jsonl
下载到本地根目录下即可:
加载模型
这里我们使用modelscope下载Qwen3-1.7B模型(modelscope在国内,所以下载不用担心速度和稳定性问题),然后把它加载到Transformers中进行训练:
train.py
import torch from modelscope import snapshot_download, AutoTokenizer from transformers import AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForSeq2Seq # Transformers加载模型权重 tokenizer = AutoTokenizer.from_pretrained("./Qwen/Qwen3-1.7B/", use_fast=False, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained("./Qwen/Qwen3-1.7B/", device_map="auto", torch_dtype=torch.bfloat16)
注意:确保下载的模型路径正确
运行python代码
python train.py
输出如下:
Loading checkpoint shards: 100%|█████████████████████████████████████████████████████████| 2/2 [00:08<00:00, 4.01s/it] Map: 100%|██████████████████████████████████████████████████████████████████| 4000/4000 [33:26<00:00, 1.99 examples/s]
没有提示报错就可以了
四、配置训练可视化工具
我们使用SwanLab来监控整个训练过程,并评估最终的模型效果。
这里直接使用SwanLab和Transformers的集成来实现
如果你是第一次使用SwanLab,那么还需要去https://swanlab.cn上注册一个账号,在用户设置页面复制你的API Key
登录SwanLab
swanlab login
输入API Key
swanlab: You can find your API key at: https://swanlab.cn/space/~/settings swanlab: Paste an API key from your profile and hit enter, or press 'CTRL + C' to quit On Windows, use Ctrl + Shift + V or right-click to paste the API key: swanlab: Login successfully. Hi, spark_xiao!
提示登录成功
五、完整代码
开始训练时的目录结构:
说明:
Qwen,存放通义千问模型文件
zh_cls_fudan-news,下载的数据集,我这里是下载的所有文件。
train.py,训练代码
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", encoding="utf-8") as file: for line in file: # 解析每一行的json数据 data = json.loads(line) context = data["text"] catagory = data["category"] label = data["output"] message = { "instruction": "你是一个文本分类领域的专家,你会接收到一段文本和几个潜在的分类选项,请输出文本内容的正确类型", "input": f"文本:{context},类型选型:{catagory}", "output": label, } 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|>system\n你是一个文本分类领域的专家,你会接收到一段文本和几个潜在的分类选项,请输出文本内容的正确类型<|im_end|>\n<|im_start|>user\n{example['input']}<|im_end|>\n<|im_start|>assistant\n", 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 # 在modelscope上下载Qwen模型到本地目录下 # model_dir = snapshot_download("qwen/Qwen2-1.5B-Instruct", cache_dir="./", revision="master") # Transformers加载模型权重 tokenizer = AutoTokenizer.from_pretrained( "./Qwen/Qwen3-1.7B/", use_fast=False, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( "./Qwen/Qwen3-1.7B/", device_map="auto", torch_dtype=torch.bfloat16) model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法 # 加载、处理数据集和测试集 train_dataset_path = "./zh_cls_fudan-news/train.jsonl" test_dataset_path = "./zh_cls_fudan-news/test.jsonl" train_jsonl_new_path = "new_train.jsonl" test_jsonl_new_path = "new_test.jsonl" if not os.path.exists(train_jsonl_new_path): dataset_jsonl_transfer(train_dataset_path, train_jsonl_new_path) if not os.path.exists(test_jsonl_new_path): dataset_jsonl_transfer(test_dataset_path, test_jsonl_new_path) # 得到训练集 train_df = pd.read_json(train_jsonl_new_path, lines=True) train_ds = Dataset.from_pandas(train_df) train_dataset = train_ds.map( process_func, remove_columns=train_ds.column_names) 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 比例 ) model = get_peft_model(model, config) args = TrainingArguments( output_dir="./output/Qwen3-zh_cls_fudan-news", per_device_train_batch_size=4, gradient_accumulation_steps=4, logging_steps=10, num_train_epochs=2, save_steps=100, learning_rate=1e-4, save_on_each_node=True, gradient_checkpointing=True, report_to="none", ) swanlab_callback = SwanLabCallback( project="Qwen3-fintune", experiment_name="Qwen3-1.7B", description="使用通义千问Qwen3-1.7B模型在zh_cls_fudan-news数据集上微调。", config={ "model": "Qwen/Qwen3-1.7B", "dataset": "swift/zh_cls_fudan-news", } ) # 开始微调 trainer = Trainer( model=model, args=args, train_dataset=train_dataset, data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), callbacks=[swanlab_callback], ) trainer.train() # 保存模型和分词器 output_dir = "./output/Qwen3-zh_cls_fudan-news" # 保存整个模型 model.save_pretrained(output_dir, save_config=True) tokenizer.save_pretrained(output_dir) # 用测试集的前10条,测试模型 test_df = pd.read_json(test_jsonl_new_path, lines=True)[:10] 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) messages.append({"role": "assistant", "content": f"{response}"}) result_text = f"{messages[0]}\n\n{messages[1]}\n\n{messages[2]}" test_text_list.append(swanlab.Text(result_text, caption=response)) swanlab.log({"Prediction": test_text_list}) swanlab.finish()
执行代码
python train.py
看到下面的进度条即代表训练开始:
swanlab: Tracking run with swanlab version 0.6.2 swanlab: Run data will be saved locally in D:\file\vllm\swanlog\run-20250610_174942-a3b1799d swanlab: 👋 Hi spark_xiao, welcome to swanlab! swanlab: Syncing run Qwen3-1.7B to the cloud swanlab: 🏠 View project at https://swanlab.cn/@spark_xiao/Qwen3-fintune swanlab: 🚀 View run at https://swanlab.cn/@spark_xiao/Qwen3-fintune/runs/4vyja2grzjuzfpu23e94a 0%| | 0/500 [00:00<?, ?it/s]` use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`. {'loss': 32.3218, 'grad_norm': 36.5546875, 'learning_rate': 9.82e-05, 'epoch': 0.04} {'loss': 10.8642, 'grad_norm': 221.5959014892578, 'learning_rate': 9.620000000000001e-05, 'epoch': 0.08} 5%|████ | 26/500 [04:58<1:28:51, 11.25s/it]
等待1小时50分钟,就完成了,输出
983531004, 'epoch': 2.0} 100%|██████████████████████████████████████████████████████████████████████████████| 500/500 [1:10:41<00:00, 8.48s/it] The attention mask is not set and cannot be inferred from input because pad token is same as eos token. As a consequence, you may observe unexpected behavior. Please pass your input's `attention_mask` to obtain reliable results. D:\file\conda\envs\my_unsloth_env\Lib\site-packages\torch\utils\checkpoint.py:86: UserWarning: None of the inputs have requires_grad=True. Gradients will be None warnings.warn( Computer Space Literature Art History Space Transport Art Economy Art swanlab: 🏠 View project at https://swanlab.cn/@spark_xiao/Qwen3-fintune swanlab: 🚀 View run at https://swanlab.cn/@spark_xiao/Qwen3-fintune/runs/4vyja2grzjuzfpu23e94a
整个过程,16GB显卡,GPU使用率在40%左右,不算太高。
注意:最后一个输出的url,就可以看到演示结果。
六、训练结果演示
在SwanLab上查看最终的训练结果:
打开链接:https://swanlab.cn/@spark_xiao/Qwen3-fintune/runs/4vyja2grzjuzfpu23e94a
可以看到在2个epoch之后,微调后的qwen3的loss降低到了不错的水平——当然对于大模型来说,真正的效果评估还得看主观效果。
可以看到在一些测试样例上,微调后的qwen3能够给出准确的文本类型:
查看环境
系统硬件
至此,你已经完成了qwen3指令微调的训练!
七、测试微调模型
完成微调训练后,会在output文件夹,生成文件夹Qwen3-zh_cls_fudan-news,这就是微调后的模型文件。
可以看一下,整个目录,大概有564MB
那么问题来了,使用的基础模型Qwen3-1.7B有3.79 GB,为什么微调保存的模型,只有564 MB
-
LoRA微调机制:
-
LoRA(Low-Rank Adaptation)是一种高效的微调方法,只训练和保存模型的一部分参数(如适配器层),而不是整个模型的所有参数。
-
微调过程中,LoRA在基础模型的每一层上添加了可训练的低秩矩阵适配器,这些适配器的参数远少于基础模型的参数。
-
-
保存内容:
-
使用
model.save_pretrained(output_dir)
保存的是LoRA适配器的参数,而不是基础模型的完整参数。基础模型的参数保持不变,因此不需要重复保存。
-
-
加载方式:
-
加载微调后的模型时,需要先加载基础模型,然后将LoRA适配器的参数合并到基础模型中。这可以通过
peft
库的PeftModel.from_pretrained()
方法实现。
-
测试模型
test.py
import torch from transformers import AutoTokenizer, AutoModelForCausalLM # 设置模型路径(微调后的模型保存路径) model_path = "./output/Qwen3-zh_cls_fudan-news" # 替换为你的模型保存路径 # model_path = "./Qwen/Qwen3-1.7B" # 加载微调后的模型和分词器 tokenizer = AutoTokenizer.from_pretrained( model_path, use_fast=False, trust_remote_code=True, model_type="qwen") model = AutoModelForCausalLM.from_pretrained( model_path, device_map="auto", torch_dtype=torch.bfloat16, trust_remote_code=True, model_type="qwen") # 开启模型评估模式 model.eval() # 准备测试数据 test_input = "近日剧宣活动上,主演之一的雷佳音询问荔枝一天能吃几颗,台下有女生回应“300颗”,显然引用了苏轼的“日啖荔枝三百颗”。然而如果真的吃掉300颗,绝对会出大问题,如果救治不及时,甚至可能导致死亡!" # 构造Prompt messages = [ {"role": "system", "content": '你是一个专业的文本分类模型,你的任务是根据给定的文本内容,从以下类型中选择最合适的分类:"娱乐", "文学", "医学", "食品", "生活", "其他"。请只输出分类结果,不要包含任何其他内容。'}, {"role": "user", "content": test_input} ] # 对输入数据进行编码 input_ids = tokenizer.apply_chat_template( messages, tokenize=True, return_tensors="pt").to(model.device) # 生成响应 outputs = model.generate( input_ids, max_new_tokens=512, # 设置最大新生成的 tokens 数量 do_sample=False, # 禁用采样,使用贪心解码 temperature=0.7, # 温度参数,值越低生成结果越确定 num_beams=5, # 设置束宽度为5 early_stopping=True # 启用提前终止 ) # 对生成的响应进行解码 response = tokenizer.decode(outputs[0], skip_special_tokens=True).strip() # 打印结果 print("测试结果:") print(response) # 提取最后的内容 last_line = response.split("\n")[-1].strip() # 获取最后一行 final_result = last_line print("\n最终分类结果:", final_result)
执行python代码
python test.py
输出:
测试结果: system 你是一个专业的文本分类模型,你的任务是根据给定的文本内容,从以下类型中选择最合适的分类:"娱乐", "文学", "医学", "食品", "生活", "其他"。请只输出分类结果,不要包含任何其他内容。 user 近日剧宣活动上,主演之一的雷佳音询问荔枝一天能吃几颗,台下有女生回应“300颗”,显然引用了苏轼的“日啖荔枝三百颗”。然而如果真的吃掉300颗,绝对会出大问题,如果救治不及时,甚至可能导致死亡! </think> 生活 最终分类结果: 生活
得到分类结果,生活。
可以看到,这一个生活常识问题,大量的吃荔枝,真的会对身体造成损害。
那么问题来了,和基础模型Qwen3-1.7B,有区别吗?
答案是有区别的,我们可以验证一下
修改test.py代码,修改以下2行,注释掉微调模型,开启模型Qwen3-1.7B
# model_path = "./output/Qwen3-zh_cls_fudan-news" # 替换为你的模型保存路径 model_path = "./Qwen/Qwen3-1.7B"
再次运行Python代码,输出如下:
测试结果: system 你是一个专业的文本分类模型,你的任务是根据给定的文本内容,从以下类型中选择最合适的分类:"娱乐", "文学", "医学", "食品", "生活", "其他"。请只输出分类结果,不要包含任何其他内容。 user 近日剧宣活动上,主演之一的雷佳音询问荔枝一天能吃几颗,台下有女生回应“300颗”,显然引用了苏轼的“日啖荔枝三百颗”。然而如果真的吃掉300颗,绝对会出大问题,如果救治不及时,甚至可能导致死亡! </think> 其他 最终分类结果: 其他
可以看到结果是其他,和微调模型输出结果是不一样的。
因为没法判断分类,所以输出其他。
八、微调模型的作用
微调模型通常是为了针对特定的行业问题或应用场景进行优化,以提高模型在特定任务上的性能和准确性。
大家也看到了,Qwen3-1.7B可以回答一些简单问题,但是涉及到专业领域,它是无法回答的。
为什么?因为它没有专业领域的相关数据。如果你给它专业领域的数据,微调出来一个模型,那么这个模型就可以解决专业领域问题。
这也是微调模型存在的意义。
你们也可以看到,一些互联网大型企业,都会自己做专业领域的模型,比如大家熟悉的,阿里,百度,字节等等。
比如阿里巴巴AI模型DAMO PANDA,这个是医疗领域的模型,它是做胰腺癌筛查的AI模型。如果应用在国内外机构,那么它的商业价值,就很高了。
根据慧博投研数据,2025年中国AI医疗市场规模预计突破349亿元,全球市场规模将在2031年达到5245.5亿美元,年复合增长率达27.7%。DAMO PANDA在胰腺癌筛查领域的突破,使其能够分得这一市场增长的红利。
针对小型企业来说,我们可以解决一些企业内部业务遇到的一些问题。
举一个简单的例子,假设你是一家手机厂商,用户购买手机出现问题,会提交工单进行处理。客服收到工单后,对这个工单进行分类处理,需要更换某些硬件,比如:屏幕,电池,相机等等。 分类之后,交给维修部门进行处理。
假设一个客服人员,每天要处理几千个工单,看花了眼,难免会出现错误。比如一年工单错误率在30%左右。
那么对于客户而言,就会造成不必要的损失。比如屏幕触摸失灵,直接更换了整块屏幕,但实际上只需要更换外屏即可,就可以节省一大笔费用。
针对这个问题,你就可以把近10年来的工单数据进行微调训练,最终得到一个90%左右准确率的微调模型,然后进行工单系统改造,上线。客服人员,就不需要工单分类了,直接交给AI模型处理。这样可以达到一个双赢的结果,客服轻松了,企业也可以更好的服务客户。
本文参考链接:
https://blog.youkuaiyun.com/SoulmateY/article/details/139564703
https://blog.youkuaiyun.com/qq_45258632/article/details/144971398
原创作者: xiao987334176 转载于: https://www.cnblogs.com/xiao987334176/p/18922128