通义千问1.5-1.8B-Chat-GPTQ-Int4实战:构建基于Transformer的文本分类模型
本文介绍了如何在星图GPU平台上自动化部署通义千问1.5-1.8B-Chat-GPTQ-Int4镜像,并基于该模型构建文本分类应用。通过高效的LoRA微调技术,开发者可以快速将该大语言模型适配于情感分析等文本分类任务,显著降低部署与定制化门槛。
通义千问1.5-1.8B-Chat-GPTQ-Int4实战:构建基于Transformer的文本分类模型
最近在尝试将开源大模型应用到具体的业务场景里,发现很多朋友对“怎么用大模型做自己的事”特别感兴趣。比如,你手头有个不错的开源模型,像通义千问,怎么让它从“能聊天”变成“能帮你做文本分类”呢?今天,我就以“文本情感分类”这个经典任务为例,带你走一遍完整的流程。
我们这次用的基座模型是通义千问1.5-1.8B-Chat的GPTQ-Int4量化版本。选择它有几个考虑:首先,1.8B的参数量对大多数开发者来说,在消费级显卡上跑起来压力不大;其次,Chat版本经过对话对齐,在理解指令和上下文方面有不错的基础;最后,GPTQ-Int4量化能大幅降低显存占用,让部署和微调变得更亲民。我们的目标,就是在这个“聪明”的基座上,教会它专注地做好“判断文本情感”这一件事。
整个过程会涵盖从数据准备、模型加载、微调训练到效果评估的每一个环节。你会发现,虽然底层是复杂的Transformer架构,但借助现代深度学习框架,整个过程可以非常清晰和直接。
1. 环境搭建与模型准备
工欲善其事,必先利其器。我们先来把环境和模型准备好。
1.1 创建虚拟环境与安装依赖
为了避免包版本冲突,建议创建一个独立的Python虚拟环境。这里以conda为例:
conda create -n qwen_finetune python=3.10
conda activate qwen_finetune
接下来安装核心依赖。我们将主要使用transformers、datasets和peft等库。transformers是Hugging Face的核心库,提供了模型加载和训练的统一接口;datasets让我们能方便地处理和加载数据;peft则用于高效参数微调,这是一种能大幅减少训练成本的技术。
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 根据你的CUDA版本调整
pip install transformers datasets accelerate peft bitsandbytes
pip install scikit-learn pandas tqdm # 用于评估和进度显示
1.2 下载与加载量化模型
通义千问1.5-1.8B-Chat-GPTQ-Int4模型可以在Hugging Face Model Hub上找到。GPTQ是一种后训练量化技术,能将模型权重压缩到4位整数(Int4),从而显著减少模型体积和推理时所需的显存,同时尽量保持精度。
使用transformers库加载这个量化模型非常简单。需要注意的是,由于是量化模型,我们需要使用专门的加载方式来处理GPTQ权重。
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
import torch
# 定义模型ID
model_id = "Qwen/Qwen1.5-1.8B-Chat-GPTQ-Int4"
# 配置4位量化加载,这对于在有限显存下加载大模型至关重要
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16, # 计算时使用半精度,节省显存并加速
bnb_4bit_use_double_quant=True, # 使用双重量化,进一步压缩
bnb_4bit_quant_type="nf4", # 使用NF4量化类型,通常有更好的精度保持
)
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto", # 自动将模型层分配到可用的GPU/CPU上
trust_remote_code=True # 信任来自远端的模型代码
)
print(f"模型加载完成,设备映射:{model.hf_device_map}")
加载成功后,模型会自动分配到你的GPU上。device_map=”auto”这个参数非常有用,它能智能地处理模型层在多个GPU甚至CPU和GPU之间的分布,尤其适合显存不那么宽裕的情况。
2. 任务定义与数据准备
我们要做的是文本情感分类,这是一个典型的序列分类任务。但通义千问原生的设计是因果语言模型(Causal LM),即根据上文预测下一个词。我们需要通过微调,让它学会在给定一段文本后,输出一个代表情感类别的标签。
2.1 理解任务适配:从生成到分类
原始的通义千问模型以对话形式工作。例如: 输入:“这部电影真好看!” 它可能会生成续写:“是的,剧情和演员表演都很出色。”
我们的目标是将其转换为分类器。我们希望输入同样的句子,模型能输出预设的标签,如“正面”。一种常见且有效的方法,是将分类任务构建成一个带有选项的文本生成任务。
我们将设计一个特定的提示模板(Prompt Template),把分类问题“伪装”成一个选择题或填空题,让模型通过生成文本来“选择”答案。
2.2 准备情感分类数据集
为了演示,我们使用一个经典的中文情感分析数据集,比如ChnSentiCorp(中文情感挖掘语料)。它包含酒店、书籍、电子产品等领域的评论,以及“正面”或“负面”的标签。
首先,我们加载并查看数据:
from datasets import load_dataset
# 这里我们假设从本地文件或通过datasets库加载一个情感数据集
# 示例:使用一个简单的模拟数据集来演示流程
import pandas as pd
# 模拟一些数据
data = {
"text": [
"这家酒店的服务态度非常好,房间也很干净。",
"产品质量太差了,用了两天就坏了,非常失望。",
"物流速度快,包装严实,给卖家点赞。",
"完全不符合描述,图片与实物严重不符。",
"操作简单,功能强大,物超所值。",
],
"label": [1, 0, 1, 0, 1] # 1: 正面, 0: 负面
}
df = pd.DataFrame(data)
# 将数据转换为datasets格式
from datasets import Dataset
dataset = Dataset.from_pandas(df)
print(dataset)
在实际项目中,你需要替换为真实、大规模的数据集,并将其划分为训练集、验证集和测试集。
2.3 构建提示模板与数据预处理
接下来是关键步骤:设计提示模板,并将原始文本和标签转换成模型训练所需的格式。
def build_classification_prompt(text, label=None):
"""
构建分类提示。
text: 原始文本
label: 可为None(推理时)或具体的标签(训练时)
"""
# 定义分类任务的指令和选项
prompt_template = """请判断以下文本的情感倾向是正面还是负面。
文本:{text}
情感倾向:"""
# 填充文本
prompt = prompt_template.format(text=text)
# 如果是训练阶段,我们需要生成“目标完成序列”
if label is not None:
# 将数字标签映射为文本
label_text = "正面" if label == 1 else "负面"
# 模型需要学习的是:在看到prompt后,生成 label_text
# 所以我们将 prompt + label_text 作为完整的“输入”
# 而在计算损失时,只对 label_text 部分进行监督
full_text = prompt + label_text
return full_text
else:
# 推理时,只返回提示部分
return prompt
# 测试提示构建
sample_text = "这部电影真精彩!"
sample_label = 1
formatted_for_training = build_classification_prompt(sample_text, sample_label)
print("训练格式示例:")
print(formatted_for_training)
print("\n推理格式示例:")
print(build_classification_prompt(sample_text))
现在,我们需要一个函数来统一处理整个数据集,包括分词(Tokenization)。分词是将文本转换成模型能理解的数字ID序列的过程。
def preprocess_function(examples):
"""
批量预处理数据。
examples: 包含‘text’和‘label’字段的数据批次
"""
# 构建完整的训练文本(提示+答案)
texts = [build_classification_prompt(t, l) for t, l in zip(examples["text"], examples["label"])]
# 分词
model_inputs = tokenizer(
texts,
max_length=512, # 设定最大长度,根据你的数据调整
truncation=True, # 过长则截断
padding="max_length", # 填充到最大长度,保证批次内长度一致
)
# 设置标签(labels)
# 对于因果语言模型微调,标签就是输入ID的副本
# 但通常我们会通过计算损失时忽略提示部分的token来只监督答案部分
# 这里我们先简单地将整个输入序列作为标签
model_inputs["labels"] = model_inputs["input_ids"].copy()
return model_inputs
# 应用预处理函数到数据集
tokenized_dataset = dataset.map(preprocess_function, batched=True)
print(f"预处理后的数据集特征:{tokenized_dataset.column_names}")
print(f"一条样本的输入ID长度:{len(tokenized_dataset[0]['input_ids'])}")
3. 高效微调策略与训练
直接微调整个拥有18亿参数的模型成本很高。我们采用参数高效微调技术,具体来说是LoRA。
3.1 LoRA简介
LoRA的核心思想非常巧妙。它不对原始的大型模型权重进行直接更新,而是为模型中的一些关键层(通常是注意力机制中的查询、键、值、输出投影矩阵)注入一组可训练的、低秩的“适配器”矩阵。在训练时,只更新这些新增的、参数量很小的适配器,而冻结原始模型的所有参数。
这样做的好处显而易见:
- 显存占用极低:只需要存储和优化适配器参数,可能只占原模型参数的0.1%-1%。
- 训练速度快:要更新的参数少了几个数量级。
- 避免灾难性遗忘:因为基座模型的知识被冻结,模型保留原有能力的同时学习新任务。
- 模块化:可以为不同任务训练不同的LoRA适配器,轻松切换。
3.2 使用PEFT配置LoRA
我们使用peft库来轻松实现LoRA。
from peft import LoraConfig, TaskType, get_peft_model
# 定义LoRA配置
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM, # 我们的任务基于因果语言模型
inference_mode=False, # 训练模式
r=8, # LoRA的秩(rank),决定适配器的大小。通常8、16、32,越小参数量越少。
lora_alpha=32, # 缩放因子,影响适配器输出的权重。
lora_dropout=0.1, # LoRA层的dropout率,防止过拟合。
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # 将LoRA适配器注入到注意力层的这些线性投影中。
bias="none", # 是否训练偏置项,通常设为"none"以节省参数。
)
# 将LoRA适配器应用到原模型上
peft_model = get_peft_model(model, lora_config)
peft_model.print_trainable_parameters() # 打印可训练参数量
执行print_trainable_parameters()后,你会看到类似这样的输出: trainable params: 4,194,304 || all params: 1,832,837,120 || trainable%: 0.2288 这意味着我们只需要训练约419万个参数,占总参数的0.23%,显存和计算需求大大降低。
3.3 配置训练参数并开始训练
接下来,我们使用Hugging Face的Trainer API来组织训练流程。
from transformers import TrainingArguments, Trainer
# 定义训练参数
training_args = TrainingArguments(
output_dir="./qwen-sentiment-lora", # 输出目录
evaluation_strategy="steps", # 按步数进行评估
eval_steps=50, # 每50步评估一次
logging_steps=10, # 每10步记录一次日志
save_strategy="steps", # 按步数保存模型
save_steps=100, # 每100步保存一次
learning_rate=2e-4, # 学习率,对于LoRA可以稍高一点
per_device_train_batch_size=4, # 每个GPU/CPU的训练批次大小
per_device_eval_batch_size=4, # 每个GPU/CPU的评估批次大小
num_train_epochs=3, # 训练轮数
weight_decay=0.01, # 权重衰减,防止过拟合
warmup_steps=50, # 预热步数,让学习率从0逐渐增加到设定值
fp16=True, # 使用混合精度训练,节省显存并加速
gradient_accumulation_steps=4, # 梯度累积步数,模拟更大的批次大小
report_to="tensorboard", # 使用TensorBoard记录日志
load_best_model_at_end=True, # 训练结束后加载最佳模型
)
# 初始化Trainer
trainer = Trainer(
model=peft_model,
args=training_args,
train_dataset=tokenized_dataset, # 实际使用时请替换为划分好的训练集
eval_dataset=tokenized_dataset, # 实际使用时请替换为划分好的验证集
tokenizer=tokenizer,
# data_collator 使用默认的即可,它负责将样本批次化
)
# 开始训练
trainer.train()
训练过程会在你的终端或TensorBoard中显示损失曲线和评估指标。由于LoRA只更新少量参数,训练通常会比较快。
4. 模型评估与推理使用
训练完成后,我们需要看看模型学得怎么样。
4.1 加载微调后的模型进行推理
训练保存的其实是LoRA适配器的权重。推理时,我们需要将基础模型和LoRA权重合并加载。
from peft import PeftModel
# 假设我们保存的适配器在 `./qwen-sentiment-lora/checkpoint-300` 目录下
lora_adapter_path = "./qwen-sentiment-lora/checkpoint-300"
# 加载基础模型(同样使用量化配置以节省显存)
base_model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True
)
# 将LoRA适配器加载到基础模型上
merged_model = PeftModel.from_pretrained(base_model, lora_adapter_path)
merged_model = merged_model.merge_and_unload() # 将适配器权重合并到原模型,之后可以像普通模型一样使用
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 现在,merged_model就是一个具备了情感分类能力的通义千问模型
4.2 构建推理函数
我们需要一个函数来处理模型的生成输出,并从中提取分类结果。
def predict_sentiment(text, model, tokenizer, max_new_tokens=10):
"""
预测单条文本的情感。
"""
# 构建推理提示(不带标签)
prompt = build_classification_prompt(text)
# 分词并移至GPU
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
# 模型生成
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens, # 限制生成新token的数量,我们只需要“正面”或“负面”
do_sample=False, # 贪婪解码,保证确定性输出
temperature=1.0,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)
# 解码生成结果
full_response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 提取生成的部分(即提示之后的内容)
generated_part = full_response[len(prompt):].strip()
# 简单的后处理:取第一个词或判断是否包含关键词
if "正面" in generated_part:
return 1, generated_part
elif "负面" in generated_part:
return 0, generated_part
else:
# 如果模型没有生成明确标签,可以返回一个默认值或进行更复杂的解析
return -1, generated_part
# 测试推理
test_texts = [
"这个产品用起来非常顺手,设计很人性化。",
"客服回应慢,问题也没解决,体验很差。",
"中规中矩吧,没什么特别的亮点。"
]
for text in test_texts:
label, response = predict_sentiment(text, merged_model, tokenizer)
sentiment = "正面" if label == 1 else "负面" if label == 0 else "未知"
print(f"文本:{text}")
print(f"模型生成:{response}")
print(f"判断情感:{sentiment}\n")
4.3 定量评估
对于测试集,我们可以进行更严谨的定量评估,计算准确率、精确率、召回率等。
from sklearn.metrics import accuracy_score, classification_report
import numpy as np
def evaluate_on_test_set(test_dataset, model, tokenizer):
"""
在测试集上评估模型性能。
test_dataset: 包含‘text’和‘label’的测试集
"""
true_labels = []
pred_labels = []
for item in test_dataset:
true_label = item["label"]
pred_label, _ = predict_sentiment(item["text"], model, tokenizer)
true_labels.append(true_label)
pred_labels.append(pred_label)
# 计算指标
accuracy = accuracy_score(true_labels, pred_labels)
report = classification_report(true_labels, pred_labels, target_names=["负面", "正面"])
print(f"测试集准确率:{accuracy:.4f}")
print("详细分类报告:")
print(report)
return accuracy, report
# 假设我们有一个划分好的测试集 `test_set`
# accuracy, report = evaluate_on_test_set(test_set, merged_model, tokenizer)
5. 总结与扩展思考
走完这一趟,你会发现基于像通义千问这样的开源大模型构建一个下游任务模型,并没有想象中那么复杂。核心思路就是任务重构与高效微调。我们把文本分类任务巧妙地包装成一个文本生成问题,然后利用LoRA这种高效的微调技术,以极低的成本让大模型掌握了新技能。
实际用下来,这套方案有几个明显的优点。首先是成本低,LoRA训练快、显存占用小,个人开发者也能玩得转。其次是效果好,大模型本身具备强大的语言理解和生成能力,作为基座起点很高,稍加微调就能在特定任务上达到不错的效果。最后是灵活,今天做情感分类,明天换个提示模板和数据集,就能让它做新闻分类、意图识别,甚至是生成特定格式的文本。
当然,过程中也会遇到一些挑战。比如,提示模板的设计很关键,设计得好,模型学得快、分得准。数据质量也至关重要,嘈杂或标注不一致的数据会严重影响效果。此外,对于更复杂的分类任务(如多标签、细粒度情感),可能需要更精细的提示工程和训练策略。
如果你已经跑通了情感分类这个例子,完全可以举一反三。比如,尝试不同的提示模板,看看哪种效果更好。或者把任务换成垃圾邮件识别、新闻主题分类,甚至是让模型学习根据商品描述生成营销文案。开源大模型就像一个“能力基座”,结合PEFT技术,我们能够以很低的门槛,为它注入各种各样的专业技能。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐



所有评论(0)