通义千问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 python3.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_4bitTrue, bnb_4bit_compute_dtypetorch.float16, # 计算时使用半精度节省显存并加速 bnb_4bit_use_double_quantTrue, # 使用双重量化进一步压缩 bnb_4bit_quant_typenf4, # 使用NF4量化类型通常有更好的精度保持 ) # 加载分词器 tokenizer AutoTokenizer.from_pretrained(model_id) # 加载量化模型 model AutoModelForCausalLM.from_pretrained( model_id, quantization_configbnb_config, device_mapauto, # 自动将模型层分配到可用的GPU/CPU上 trust_remote_codeTrue # 信任来自远端的模型代码 ) 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, labelNone): 构建分类提示。 text: 原始文本 label: 可为None推理时或具体的标签训练时 # 定义分类任务的指令和选项 prompt_template 请判断以下文本的情感倾向是正面还是负面。 文本{text} 情感倾向 # 填充文本 prompt prompt_template.format(texttext) # 如果是训练阶段我们需要生成“目标完成序列” 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_length512, # 设定最大长度根据你的数据调整 truncationTrue, # 过长则截断 paddingmax_length, # 填充到最大长度保证批次内长度一致 ) # 设置标签labels # 对于因果语言模型微调标签就是输入ID的副本 # 但通常我们会通过计算损失时忽略提示部分的token来只监督答案部分 # 这里我们先简单地将整个输入序列作为标签 model_inputs[labels] model_inputs[input_ids].copy() return model_inputs # 应用预处理函数到数据集 tokenized_dataset dataset.map(preprocess_function, batchedTrue) 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_typeTaskType.CAUSAL_LM, # 我们的任务基于因果语言模型 inference_modeFalse, # 训练模式 r8, # LoRA的秩rank决定适配器的大小。通常8、16、32越小参数量越少。 lora_alpha32, # 缩放因子影响适配器输出的权重。 lora_dropout0.1, # LoRA层的dropout率防止过拟合。 target_modules[q_proj, k_proj, v_proj, o_proj], # 将LoRA适配器注入到注意力层的这些线性投影中。 biasnone, # 是否训练偏置项通常设为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的TrainerAPI来组织训练流程。from transformers import TrainingArguments, Trainer # 定义训练参数 training_args TrainingArguments( output_dir./qwen-sentiment-lora, # 输出目录 evaluation_strategysteps, # 按步数进行评估 eval_steps50, # 每50步评估一次 logging_steps10, # 每10步记录一次日志 save_strategysteps, # 按步数保存模型 save_steps100, # 每100步保存一次 learning_rate2e-4, # 学习率对于LoRA可以稍高一点 per_device_train_batch_size4, # 每个GPU/CPU的训练批次大小 per_device_eval_batch_size4, # 每个GPU/CPU的评估批次大小 num_train_epochs3, # 训练轮数 weight_decay0.01, # 权重衰减防止过拟合 warmup_steps50, # 预热步数让学习率从0逐渐增加到设定值 fp16True, # 使用混合精度训练节省显存并加速 gradient_accumulation_steps4, # 梯度累积步数模拟更大的批次大小 report_totensorboard, # 使用TensorBoard记录日志 load_best_model_at_endTrue, # 训练结束后加载最佳模型 ) # 初始化Trainer trainer Trainer( modelpeft_model, argstraining_args, train_datasettokenized_dataset, # 实际使用时请替换为划分好的训练集 eval_datasettokenized_dataset, # 实际使用时请替换为划分好的验证集 tokenizertokenizer, # 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_configbnb_config, device_mapauto, trust_remote_codeTrue ) # 将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_tokens10): 预测单条文本的情感。 # 构建推理提示不带标签 prompt build_classification_prompt(text) # 分词并移至GPU inputs tokenizer(prompt, return_tensorspt).to(model.device) # 模型生成 with torch.no_grad(): outputs model.generate( **inputs, max_new_tokensmax_new_tokens, # 限制生成新token的数量我们只需要“正面”或“负面” do_sampleFalse, # 贪婪解码保证确定性输出 temperature1.0, pad_token_idtokenizer.pad_token_id, eos_token_idtokenizer.eos_token_id, ) # 解码生成结果 full_response tokenizer.decode(outputs[0], skip_special_tokensTrue) # 提取生成的部分即提示之后的内容 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星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。