跳至主要内容

使用 QLoRA 微调 Qwen3.5 Small 进行新闻分类

了解如何使用 QLoRA 在轻量环境下微调 Qwen3.5-0.8B,从而显著提升新闻分类性能。
· 8分钟 阅读

使用 AI 探索

Qwen3.5 Small 是一个适合轻量级微调的实用模型,专为原型验证、任务特定适配和科研工作流而设计。结合 QLoRA,我们可以在冻结的 4 位量化基座模型上进行微调,使过程更加省内存,并能在受限硬件上顺畅运行。

本教程将完成环境配置、加载模型与 AG News 数据集、评估基座模型,然后对 Qwen3.5 small 进行新闻分类微调。最后,我们会对比训练前后的结果,并将 LoRA 适配器保存并发布到 Hugging Face 以便复用。 

如果您还不熟悉其工作方式,建议先学习 Hugging Face 基础技能路径。

什么是 Qwen3.5 Small?

在编写代码前,先了解一下我们要使用的模型。Qwen3.5 是一个基础模型家族,“Small” 系列指其轻量级产品线,具体为小于 100 亿参数的模型。本教程将使用 0.8B 版本。

尽管只有 8 亿参数,Qwen3.5-0.8B 依然能力出众且极其高效。以 4 位精度加载时,所需显存不足 2GB。这让它非常适合消费者级笔记本、Kaggle 等免费云环境,以及轻量的边缘部署。

此外,Qwen3.5 架构支持高达 262K 的超长上下文窗口和原生多模态。尽管本指南专注于文本分类,但我们讲解的基础概念也能轻松扩展到视觉任务。该模型还具备混合推理模式,不过为了最大化我们这个直观标注任务的推理速度,我们将其保持禁用。

总体而言,Qwen3.5-0.8B 对于原型验证非常实用。它为我们提供了一种快速、低成本的方式来测试微调工作流,同时还能在下游任务中取得不错的效果。

1. 配置 Kaggle 环境

本教程将使用 Kaggle 笔记本作为训练环境。Kaggle 提供 GPU 资源,使其成为训练、微调和评估模型的便捷平台。运行笔记本前,先新建一个 Kaggle 笔记本,打开Session 设置,并选择 GPU P100 作为加速器。 

接下来,我们将完成环境配置、向 Kaggle 添加 Hugging Face 令牌,并导入本教程中用到的 Python 库。

创建 Hugging Face 访问令牌

在运行笔记本之前,请在您的帐户设置中创建一个 Hugging Face 访问令牌。进入Settings,打开Access Tokens,并生成一个新令牌。如果您希望在教程末尾将微调后的 LoRA 适配器发布到 Hugging Face Hub,请确保令牌具备写入权限。

Hugging Face Access Token Page

将令牌添加到 Kaggle secrets

生成令牌后,打开您的 Kaggle 笔记本并将其添加为 secret。在笔记本编辑器中进入Add-ons,然后选择Secrets,创建一个名为 HUGGINGFACE_TOKEN 的新 secret。将令牌值粘贴到那里,这样笔记本就能在不暴露到代码中的情况下安全访问它。

Kaggle Secrets setup

安装所需库

现在我们可以安装本工作流所需的主要库。这些包涵盖模型加载、量化、参数高效微调、数据集处理和评估。

!pip install -q -U transformers accelerate
!pip install -q peft trl bitsandbytes datasets scikit-learn

使用 Hugging Face 进行身份验证

接下来,我们从 Kaggle secrets 加载 Hugging Face 令牌并用于认证。这样笔记本就能访问模型,并在之后推送已训练的适配器。

from huggingface_hub import login
from kaggle_secrets import UserSecretsClient

user_secrets = UserSecretsClient()
login(token=user_secrets.get_secret("HUGGINGFACE_TOKEN"))

导入 Python 库

最后,我们导入整个教程中用于模型加载、训练、数据集处理与评估的 Python 库。

import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig
from transformers import TrainingArguments
from sklearn.metrics import accuracy_score, f1_score
from tqdm.notebook import tqdm

2. 加载 Qwen3.5 模型与 AG News 数据集

环境就绪后,我们可以加载基座模型并准备数据集进行微调。 

本节将以 4 位精度加载 Qwen3.5-0.8B 以提高训练的内存效率,完成分词器设置,并加载一小部分 AG News 数据集用于训练与评估。

以 4 位量化加载 Qwen3.5-0.8B

我们首先使用 BitsAndBytes 以 4 位量化方式加载 Qwen3.5-0.8B。这能减少内存占用,使模型在 Kaggle 笔记本环境中更易使用。

model_id = "Qwen/Qwen3.5-0.8B"

bnb_config = BitsAndBytesConfig(
  load_in_4bit=True,
  bnb_4bit_quant_type="nf4",
  bnb_4bit_compute_dtype=torch.bfloat16
)

model = AutoModelForCausalLM.from_pretrained(
  model_id,
  quantization_config=bnb_config,
  device_map="auto",
  dtype=torch.bfloat16
)

加载分词器

接着,我们为同一模型加载分词器。我们还将填充符设置为序列结束符,以便模型在训练与生成时正确处理填充输入。

tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token

model.config.pad_token_id = tokenizer.eos_token_id
model.generation_config.pad_token_id = tokenizer.eos_token_id

加载 AG News 数据集

最后,我们加载 AG News 数据集,并为本教程创建较小的训练与测试切分。使用子集能加快流程,同时保留足够的数据来评估微调效果。

dataset = load_dataset("ag_news")

train_dataset = dataset["train"].select(range(2500))
test_dataset = dataset["test"].select(range(200))

3. 提示词格式与基线评估

在微调之前,我们需要定义模型看到的任务形式,并衡量其原始表现。 

本节将创建用于新闻分类的指令式提示词,从模型输出中提取预测,对单个样本进行推理,并在训练前对测试集评估基座模型。

创建指令提示词

我们先定义标签名称,并构建一个要求模型对新闻进行分类的提示词。该提示词要求模型仅返回数值标签,便于在评估时解析输出。

labels = ['World','Sports','Business','Sci/Tech']

def build_prompt(text):

    return f"""
Classify the news article.

Article:
{text}

Return ONLY the number of the correct label.

0 = World
1 = Sports
2 = Business
3 = Sci/Tech

Answer:
"""

从模型输出中提取预测

接着,我们编写一个小工具函数,从生成文本中提取预测标签。该函数会移除多余的推理标记,并将最终类别标签以整数形式返回。

import re

def extract_label(text):

   text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
   matches = re.findall(r"[0-3]", text)

   if matches:
       return int(matches[-1])

   return -1

运行一个示例推理

在对完整测试集评估前,我们先在一个示例上试运行。这有助于我们查看文章内容、核对正确标签,并了解微调前模型的预测。

sample = train_dataset[10]

sample_text = sample["text"]
sample_label = sample["label"]

print("Ground truth label:", sample_label)
print("Sample text:")
print(sample_text[:300])

输出显示真实标签为 2,对应于Business。

Ground truth label: 2
Sample text:
Oil and Economy Cloud Stocks' Outlook  NEW YORK (Reuters) - Soaring crude prices plus worries  about the economy 
and the outlook for earnings are expected to  hang over the stock market next week during the depth of the  summer doldrums.

接下来,我们格式化提示词,将其传给模型,并生成一个预测。

prompt = build_prompt(sample_text)

inputs = tokenizer(prompt, return_tensors="pt").to("cuda")

with torch.inference_mode():
    outputs = model.generate(
        **inputs,
        max_new_tokens=5,
        do_sample=False,
        pad_token_id=tokenizer.eos_token_id
    )

prediction = tokenizer.decode(outputs[0], skip_special_tokens=True)

print("Model prediction (before training):")
print(extract_label(prediction))

模型同样预测为 2,即正确分类。这样我们就快速确认了提示词格式与预测流程工作正常。

Model prediction (before training):
2

微调前评估基座模型

现在我们在测试集上评估基座模型,建立一个基线。这为对比微调前后的性能提供参考点。

def evaluate_model(dataset):

    preds = []
    refs = []

    model.eval()

    for example in tqdm(dataset):

        text = example["text"]
        label = example["label"]

        prompt = build_prompt(text)

        inputs = tokenizer(prompt, return_tensors="pt", truncation=True)
        inputs = {k: v.to("cuda") for k, v in inputs.items()}

        with torch.inference_mode():

            outputs = model.generate(
                **inputs,
                max_new_tokens=5,
                do_sample=False,
                pad_token_id=tokenizer.eos_token_id
            )

        prediction = tokenizer.decode(outputs[0], skip_special_tokens=True)

        pred_label = extract_label(prediction)

        preds.append(pred_label)
        refs.append(label)

    acc = accuracy_score(refs, preds)
    f1 = f1_score(refs, preds, average="weighted")

    print("Accuracy:", acc)
    print("F1:", f1)


print('Pre-fine-tuning evaluation')
evaluate_model(test_dataset)

输出显示准确率为 0.52,加权F1 分数为 0.4589。这些基线结果表明模型已具备一定任务能力,但仍有明显提升空间,值得通过微调改进。

Pre-fine-tuning evaluation
Accuracy: 0.52
F1: 0.45889163289429247

4. 使用 QLoRA 微调 Qwen3.5

基线建立后,我们可以在新闻分类任务上微调模型。本节将格式化训练数据,配置LoRA 适配器,定义训练设置,训练模型,并保存最终适配器以供后续使用。

准备训练数据

首先,我们将每个训练样本格式化为与推理时相同的提示词结构。我们把正确标签附加到提示词末尾,便于模型在微调中学习生成正确答案。

def format_train(example):
    prompt = build_prompt(example['text'])
    answer = str(example['label'])
    return {'text': prompt + ' ' + answer}

train_dataset = train_dataset.map(format_train)

配置 LoRA 适配器

接着我们定义 LoRA 配置,用于指定微调时需要适配的网络层,从而只训练一小部分新增参数,而非更新整个模型。

lora_config = LoraConfig(
    r=16,
    lora_alpha=16,
    lora_dropout=0,
    bias='none',
    task_type='CAUSAL_LM',
    target_modules=[
        'q_proj','k_proj','v_proj','o_proj',
        'gate_proj','up_proj','down_proj'
    ]
)

定义训练配置

随后我们设置有监督微调的训练参数。这些设置控制每次处理的样本数量、梯度累积方式、训练时长以及输出文件的保存位置。

training_args = SFTConfig(
    output_dir="./qwen35-lora",
    per_device_train_batch_size=8,
    gradient_accumulation_steps=2,
    learning_rate=2e-4,
    num_train_epochs=1,
    logging_steps=10,
    bf16=True,
    dataset_text_field="text",
    packing=False,
)

训练模型

现在,我们用模型、数据集、LoRA 配置、训练设置和分词器来初始化训练器,并在格式化后的 AG News 训练集上启动微调。

trainer = SFTTrainer(
    model=model,
    train_dataset=train_dataset,
    peft_config=lora_config,
    args=training_args,
    processing_class=tokenizer
)
trainer.train()

输出: 

TrainOutput(global_step=157, training_loss=1.9094241804378047, metrics={'train_runtime': 616.477, 'train_samples_per_second': 4.055, 'train_steps_per_second': 0.255, 'total_flos': 1041693973114368.0, 'train_loss': 1.9094241804378047})

保存已训练适配器

训练结束后,我们保存 LoRA 适配器和分词器。这样便可在后续进行评估、分享,或发布到 Hugging Face。

trainer.model.save_pretrained("qwen35-small-news-class")
tokenizer.save_pretrained("qwen35-small-news-class")

5. 评估微调后的模型

训练完成后,我们重新加载基座模型并挂载已保存的 LoRA 适配器。这样就能使用微调权重进行评估,并与先前的基线结果对比。

重新加载基座模型并挂载 LoRA 适配器

我们先再次加载基座 Qwen3.5 模型,然后挂载保存在 Kaggle 工作目录中的微调 LoRA 适配器。之后切换到评估模式,并查看可训练参数汇总。

from transformers import AutoModelForCausalLM
base_model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map="auto",
    dtype=torch.bfloat16
)


from peft import PeftModel

model = PeftModel.from_pretrained(
    base_model,
    "/kaggle/working/qwen35-small-news-class"
)

model.eval()
model.print_trainable_parameters()

输出显示此时没有可训练参数,因为训练已完成,我们现在使用保存的适配器进行推理与评估。

trainable params: 0 || all params: 758,782,784 || trainable%: 0.0000

评估微调后的模型

接下来,我们在测试集上再次运行相同的评估函数,衡量微调后模型的提升幅度。

print('Post-fine-tuning evaluation')
evaluate_model(test_dataset)

输出显示准确率为 0.865,加权 F1 分数为 0.8661。与基线相比,微调后有显著提升。

Post-fine-tuning evaluation
Accuracy: 0.865
F1: 0.8660926258753361

对比结果

我们现在可以对比微调前后模型的表现。准确率和 F1 分数都有大幅提升,表明 QLoRA 微调有助于模型很好地适配新闻分类任务。

指标

训练前

训练后

Accuracy

0.52

0.865

F1 Score

0.4589

0.866

可见,使用 QLoRA 微调 Qwen3.5 Small 能显著提升分类性能。

6. 将模型发布到 Hugging Face

评估完成后,我们可以将微调后的适配器与分词器上传到 Hugging Face。这便于在后续项目中复用、分享和加载,而无需重复完整的训练流程。

model.push_to_hub("kingabzpro/qwen35-small-news-class")
tokenizer.push_to_hub("kingabzpro/qwen35-small-news-class")

上传完成后,模型将可在以下地址访问:

Model:
https://huggingface.co/kingabzpro/qwen35-small-news-class

Fine-tuned Qwen 3.5 LoRA on Hugging Face

来源:kingabzpro/qwen35-small-news-class

本教程对应的Kaggle 笔记本也已提供。建议仅在复现上述步骤遇到问题时使用。您也可以克隆该笔记本并在本地系统上运行,但请记得将 Hugging Face 用户名与令牌更新为您自己的账户信息。

7. 总结

在本教程中,我们使用 Qwen3.5-0.8B 结合 QLoRA 进行新闻分类微调,在非常轻量的设置下取得了很强的结果。仅用一个 epoch、小规模数据,准确率从 0.52 提升到 0.865,加权 F1 从 0.4589 提升到 0.8661。 

对这样一个小模型而言,最令人惊喜的是:我们不需要巨大的模型、庞大的数据集或漫长的训练,也能取得很强的下游效果。

更实用的是其整体配置。Qwen 将 0.8B 模型定位为适合原型验证与任务特定微调,而 QLoRA 通过在冻结的4 位基座模型之上训练适配器来保持低内存占用。这不仅适用于云端笔记本工作流,对小型消费级与笔记本 GPU 的测试同样现实可行。

另一个令人期待的点在于Qwen3.5 系列同时支持文本与视觉微调。因此,尽管我们在此用于文本分类,同样的思路也能扩展到图像分类、图像转文本和文本到文本任务。这让这样的小模型在快速验证想法、低成本微调且保持轻量部署方面非常实用。

也推荐您查看我们的其他资源,包括Qwen3.5 指南以及如何在单张 GPU 上本地运行 Qwen3.5 的教程。

FAQs

为什么在 0.8B 模型上使用 QLoRA(4 位量化)而不是全量微调?

尽管按大模型标准 0.8B 被视为“较小”,但完全更新全部 8 亿参数仍需要可观的显存和算力。QLoRA 将 4 位精度的基座模型冻结,只在其之上训练一小组新的适配器权重。这会大幅降低内存占用,使您可以在免费的 Kaggle P100 GPU 或普通消费级笔记本上从容训练而不至于内存溢出。

Qwen3.5 支持新的“思考”模式。是否应在分类任务中启用它?

Qwen3.5-0.8B 原生支持混合推理,但默认以“非思考”模式运行。虽然可以启用其推理能力,但像 AG News 这样的简单分类任务并不需要复杂的逐步逻辑。对于这种直接标注的任务,保持关闭思考模式会更快且更省 token。

Qwen3.5 的 262K 超长上下文窗口会如何影响本微调脚本?

虽然基座模型原生支持最多 262,144 个 token,但在超长文档上进行微调会很快耗尽 Kaggle GPU 的内存。对新闻分类而言,文本较短,因此这不是问题。如果您计划将此脚本用于长文档任务(例如整本书的摘要),需要启用梯度检查点并降低批大小。

Abid Ali Awan

作者

Abid Ali Awan

作为一名持证的数据科学家,我热衷于利用前沿技术打造创新的机器学习应用。凭借在语音识别、数据分析与报告、MLOps、对话式人工智能以及自然语言处理方面的扎实背景,我不断打磨构建智能系统的能力,力求带来切实影响。除技术专长外,我也擅长沟通,能够将复杂概念提炼为清晰、简明的表述。因此,我成为数据科学领域备受关注的博主,与不断壮大的数据专业人士社区分享洞见与实践经验。目前,我专注于内容创作与编辑,借助大语言模型打造有力且吸引人的内容,帮助企业与个人更好地发挥数据价值。

主题