公司动态

文本分类的三种解法:接口调用、Bert微调与LLM微调实战对比

📅 2026/7/17 5:25:18
文本分类的三种解法:接口调用、Bert微调与LLM微调实战对比
1. 文本分类的应用场景文本分类是自然语言处理中最基础也最广泛落地的任务之一。简单来说它的目标就是给定一段文本让模型判断它属于哪个类别。内容平台今日头条把新闻自动归入科技“娱乐”体育等频道靠的就是文本分类知乎、小红书对帖子打标签推荐给可能感兴趣的用户电商与客服淘宝评论自动识别好评/差评情感分析本质也是文本分类客服系统把工单分派给对应部门我要退货→售后组怎么用→技术支持组风控与合规社交媒体自动检测违规内容涉黄、涉暴、广告引流企业内审系统扫描邮件标记敏感信息信息处理邮件客户端的垃圾邮件过滤二分类的经典应用法律文书、医疗报告的自动归档这些场景有一个共同点文本量大、类别相对固定、需要快速准确地处理。这也正是文本分类任务的价值所在——用模型替代人工实现规模化、自动化。2. 数据准备与探索分析2.1 数据集获取与构建这三种方法都使用CLUE/TNEWS这个数据集。fromdatasetsimportload_dataset dsload_dataset(clue,tnews)下载下来的数据文件有四个label_map.json类别映射表ID ↔ 类别码 ↔ 中文名 train.json训练集53K 条带标签模型学习用 val.json验证集10K 条带标签调参和最终评测用 test.json测试集10K 条标签全为 -1不可用忽略train.jsonval.json 里面的数据类似这样label带的数字表示类型7是“教育”{ idx: 0, sentence: 上课时学生手机响个不停老师一怒之下把手机摔了家长拿发票让老师赔大家怎么看待这种事, label: 7 }2.2 数据探索对数据进行分析后可以得出以下结论类别不均衡比 23x科技 5955 条 vs 证券 257 条文本极短均值 22 字P99 39 字max_length64 已完全覆盖Token/字符比值 ≈ 0.97中文基本 1 字 1 token3. 方案一调LLM接口。真的划算吗3.1 提示词设计与调用流程采用 deepseek-v4-pro 和 deepseek-v4-flash 的非思考模型先看预测的准确率。提示词如下# src_llm/deepseek_llm.py# 15 个类别名LABEL_NAMES[故事,文化,娱乐,体育,财经,房产,汽车,教育,科技,军事,旅游,国际,证券,农业,电竞,]SYSTEM_PROMPT(你是一个新闻标题分类助手。请将给定的新闻标题分类到以下类别之一只输出类别名称不要输出任何其他内容。\n可选类别、.join(LABEL_NAMES))defclassify_one(text:str,client:OpenAI,max_tokens:int16)-str:调用 DeepSeek-V4-pro返回模型输出。responseclient.chat.completions.create(modeldeepseek-v4-pro,messages[{role:system,content:SYSTEM_PROMPT},{role:user,content:build_prompt(text)},],max_tokensmax_tokens,temperature0,extra_body{thinking:{type:disabled}})returnresponse.choices[0].message.content.strip()3.2 调用云端llm接口的测试效果deepseek-v4-pro关闭思考测试200条输出 DeepSeek-V4-PRO (deepseek-v4-pro) Zero-Shot 分类结果 样本数 : 200 准确率 : 106/200 0.5300 无法解析 : 3 条 (1.5%) 总耗时 : 168.2s, 均值 0.84s/条deepseek-v4-flash关闭思考测试200条输出 DeepSeek-V4-Flash (deepseek-v4-flash) Zero-Shot 分类结果 样本数 : 200 准确率 : 104/200 0.5200 无法解析 : 0 条 (0.0%) 总耗时 : 126.8s, 均值 0.63s/条3.3 接口方案的局限性虽然说调用云端接口这个方案很简单但是从刚才的实验结果中能看出这个方案存在以下局限性准确率天花板明显。DeepSeek-V4-Pro 已经是市面顶尖模型但在新闻标题分类上 zero-shot 准确率仅 53%。标题短、语义模糊确实增加了难度但更关键的是接口方案只能靠 prompt 调优无法针对任务做任何模型层面的优化天花板触手可及。响应速度慢。V4-Flash 单条耗时 0.63 秒200 条就跑了两分多钟。一旦业务量上到 10 万条需要超过 16 个小时。输出不可控。LLM 本质是逐 token 生成提示词无法 100% 约束格式。V4-Pro 实验中就有 1.5% 的输出无法解析——在生产环境中这就是故障点需要额外写解析逻辑来兜底。成本随规模线性增长。200 条的 API 费用看起来不起眼但 10 万条就是 500 倍。而本地部署一次训练后推理几乎零成本。对于长期、大批量的分类任务接口方案的经济账很难算过来。4. 方案二Bert微调4.1 Bert是什么这个实验用到的模型叫 bert-base-chinese。它是Google 用海量无标注文本中文维基、新闻等训练好一个通用模型学会了中文的语法、语义、常识。然后使用者在通用模型的基础上用自己的标注数据如 TNEWS 新闻标题再训练几轮让它适配具体的任务。Bert 只用了 Transformer 的 Encoder 部分。可以把它理解为一个文本理解器——输入一段文本输出每个词的向量表示这些向量已经融合了上下文信息。参数规模约 102M1.02 亿输入新闻标题如苹果发布了最新的 iPhone 17输出一个 768 维的向量取 CLS 位置或其他池化方式然后接一个分类头输出 15 个类别的概率4.2 微调的核心代码使用bert的流程如下然后在训练的过程中对bert模型是全量微调并且对bert层和分类头采用的是不同的学习率bert的学习率会小一些尽量别影响已有的能力。分词器核心代码# src/dataset.pyclassTNEWSDataset(Dataset): 每条样本经过 tokenizer 处理后返回 input_ids : [max_length] — token id 序列含 [CLS] 和 [SEP] attention_mask : [max_length] — 1真实 token0padding token_type_ids : [max_length] — 单句分类全为 0 label : scalar — 类别 idtest 集为 -1 def__init__(self,data_path:Path,tokenizer:BertTokenizer,max_length:int128,):withopen(data_path,encodingutf-8)asf:self.datajson.load(f)self.tokenizertokenizer self.max_lengthmax_lengthdef__len__(self):returnlen(self.data)def__getitem__(self,idx):itemself.data[idx]encodingself.tokenizer(item[sentence],max_lengthself.max_length,truncationTrue,# 超出 max_length 时截断paddingmax_length,# 不足 max_length 时用 [PAD] 填充return_tensorspt,# 直接返回 PyTorch tensor)return{input_ids:encoding[input_ids].squeeze(0),# [max_length]attention_mask:encoding[attention_mask].squeeze(0),# [max_length]token_type_ids:encoding[token_type_ids].squeeze(0),# [max_length]label:torch.tensor(item[label],dtypetorch.long),}train_dsTNEWSDataset(data_dir/train.json,tokenizer,max_length)val_dsTNEWSDataset(data_dir/val.json,tokenizer,max_length)train_loaderDataLoader(train_ds,batch_sizebatch_size,shuffleTrue,num_workersnum_workers)val_loaderDataLoader(val_ds,batch_sizebatch_size,shuffleFalse,num_workersnum_workers)模型的forward计算# src/model.pydefforward(self,input_ids:torch.Tensor,# [B, L]attention_mask:torch.Tensor,# [B, L]token_type_ids:torch.Tensor,# [B, L])-torch.Tensor: 返回 logits: [B, num_labels]未经 softmax交叉熵 loss 内部做 # return_dictTruetransformers 5.x 默认返回 tuple显式要求返回命名对象outputsself.bert(input_idsinput_ids,attention_maskattention_mask,token_type_idstoken_type_ids,return_dictTrue,)# last_hidden_state : [B, L, H]# pooler_output : [B, H] — BERT 对 [CLS] 过了一层 tanh 的版本last_hiddenoutputs.last_hidden_state# [B, L, H]vecself._pool(last_hidden,attention_mask)# [B, H]vecself.dropout(vec)logitsself.classifier(vec)# [B, num_labels]returnlogits训练的代码有几点关注下加权 Loss让小样本也被看见科技 5955 条 vs 证券 257 条# src/train.pyifargs.use_class_weight:weightscompute_loss_weights(data_dir,num_labels,device)criterionnn.CrossEntropyLoss(weightweights)分层学习率预训练部分稳着调新加部分大步学# src/train.pybert_paramslist(model.bert.parameters())head_paramslist(model.classifier.parameters())list(model.dropout.parameters())optimizerAdamW([{params:bert_params,lr:2e-5},# Bert 层小学习率{params:head_params,lr:2e-5*5.0},# 分类头大 5 倍])Warmup先热身再冲刺。训练刚开始时 loss 很大、梯度很陡如果一上来就用满学习率可能把预训练权重冲坏。warmup 让学习率从 0 线性增长到目标值前 10% 步数用来热身。# src/train.pywarmup_stepsint(total_steps*0.1)schedulerget_linear_schedule_with_warmup(optimizer,num_warmup_stepswarmup_steps,num_training_stepstotal_steps)池化策略怎么把 token 矩阵压缩成句向量。Bert 最后一层输出的是一个 [B, L, 768] 的矩阵——每条文本有 L 个 token每个 token 是 768 维向量。分类头需要的是一个句向量也就是把 L 个向量压缩成一个。本项目支持三种策略# src/train.py CLS直接取 [CLS] 位置的输出Bert 预训练时专门优化过最经典 Mean所有真实 token 取平均每个词平等贡献实践中往往更鲁棒 Max每个维度取最大值保留最显著信号适合关键词驱动的场景 ifself.poolcls:returnlast_hidden[:,0,:]# 取 [CLS] 位置最经典的做法ifself.poolmean:sum_hidden(last_hidden*mask).sum(dim1)# 有效 token 求和countmask.sum(dim1).clamp(min1e-9)# 有效 token 数量returnsum_hidden/count# 求均值信息利用更充分ifself.poolmax:maskedlast_hidden(1-mask)*(-1e9)# padding 位置设为负无穷returnmasked.max(dim1).values# 取最大值保留最显著特征4.3 最终效果与混淆矩阵解读接着进行四次脚本训练对比下。python train.py--poolcls--epochs3python train.py--poolcls--epochs3--use_class_weightpython train.py--poolmean--epochs3--use_class_weightpython train.py--poolmax--epochs3--use_class_weight先用不带use_class_weight的验证让小样本也被看见再对比下三种池化策略。列名 含义 直白理解 precision 精确率 模型猜这个类别时猜对的概率 recall 召回率 真实属于这个类别的样本被找到的比例 f1-score F1 分数 precision 和 recall 的调和平均综合打分 support 样本数 验证集中这个类别有多少条cls不处理类别不均分类报告 precision recall f1-score support 故事 0.51 0.45 0.48 215 文化 0.53 0.55 0.54 736 娱乐 0.56 0.60 0.58 910 体育 0.73 0.72 0.72 767 财经 0.49 0.49 0.49 956 房产 0.62 0.62 0.62 378 汽车 0.70 0.62 0.66 791 教育 0.57 0.58 0.57 646 科技 0.53 0.54 0.54 1089 军事 0.56 0.54 0.55 716 旅游 0.50 0.49 0.49 693 国际 0.50 0.54 0.52 905 证券 0.52 0.31 0.39 45 农业 0.51 0.54 0.52 494 电竞 0.68 0.63 0.66 659 accuracy 0.57 10000 macro avg 0.57 0.55 0.56 10000 weighted avg 0.57 0.57 0.57 10000cls:分类报告 precision recall f1-score support 故事 0.43 0.69 0.53 215 文化 0.52 0.52 0.52 736 娱乐 0.62 0.54 0.58 910 体育 0.73 0.71 0.72 767 财经 0.50 0.46 0.48 956 房产 0.54 0.69 0.60 378 汽车 0.68 0.64 0.66 791 教育 0.55 0.61 0.58 646 科技 0.58 0.44 0.50 1089 军事 0.53 0.64 0.58 716 旅游 0.48 0.53 0.50 693 国际 0.52 0.42 0.46 905 证券 0.34 0.58 0.43 45 农业 0.48 0.62 0.54 494 电竞 0.66 0.65 0.65 659 accuracy 0.56 10000 macro avg 0.54 0.58 0.56 10000 weighted avg 0.57 0.56 0.56 10000 val accuracy : 0.5616 val macro F1 : 0.5560mean:分类报告 precision recall f1-score support 故事 0.43 0.63 0.51 215 文化 0.53 0.54 0.54 736 娱乐 0.61 0.55 0.58 910 体育 0.74 0.71 0.72 767 财经 0.50 0.45 0.48 956 房产 0.54 0.70 0.61 378 汽车 0.68 0.64 0.65 791 教育 0.52 0.63 0.57 646 科技 0.57 0.44 0.50 1089 军事 0.53 0.59 0.56 716 旅游 0.50 0.50 0.50 693 国际 0.51 0.43 0.47 905 证券 0.36 0.71 0.48 45 农业 0.48 0.62 0.54 494 电竞 0.65 0.68 0.66 659 accuracy 0.56 10000 macro avg 0.54 0.59 0.56 10000 weighted avg 0.57 0.56 0.56 10000 val accuracy : 0.5606 val macro F1 : 0.5575max:分类报告 precision recall f1-score support 故事 0.39 0.69 0.50 215 文化 0.54 0.52 0.53 736 娱乐 0.64 0.51 0.57 910 体育 0.72 0.72 0.72 767 财经 0.49 0.38 0.43 956 房产 0.52 0.72 0.60 378 汽车 0.68 0.66 0.67 791 教育 0.54 0.60 0.57 646 科技 0.55 0.46 0.50 1089 军事 0.52 0.63 0.57 716 旅游 0.49 0.51 0.50 693 国际 0.50 0.42 0.45 905 证券 0.31 0.71 0.43 45 农业 0.46 0.60 0.52 494 电竞 0.64 0.68 0.66 659 accuracy 0.55 10000 macro avg 0.53 0.59 0.55 10000 weighted avg 0.56 0.55 0.55 10000 val accuracy : 0.5549 val macro F1 : 0.5488分析通过对比cls是否带加权。不加权时证券类 Recall 只有 31%加权后涨到 58%几乎翻倍。故事类从 45% 涨到 69%房产类从 62% 涨到 69%。代价是 Accuracy 降了 1 个百分点以及证券类的 Precision 从 0.52 降到 0.34。Precision 下降恰恰说明加权 loss 在起作用模型不再逃避少数类遇到吃不准的也敢猜了。对于每个类别都同等重要的业务场景这 1% 的 Accuracy 换少数类 Recall 的大幅提升完全值得。锁定加权 loss对比 CLS、Mean、Max 三种池化。三者在整体指标上差距很小Accuracy 相差不到 1 个百分点说明对于新闻标题这种短文本分类任务池化策略不是决定性因素。Mean 在 Macro F1 上略优Max 在证券 Recall 上和 Mean 持平0.71但在财经类上表现最差0.38。CLS 作为默认策略各方面表现均衡。5. 方案三LLM私有化微调5.1 全量微调5.1.1 训练流程和我上一篇minigpt的SFT训练大体一样。构建大模型输出的格式loss只计算回答部分。喂给大模型的数据长这样|im_start|system 你是一个新闻标题分类助手。请将给定的新闻标题分类到以下类别之一只输出类别名称不要输出任何其他内容。 可选类别故事、文化、娱乐、体育、财经、房产、汽车、教育、科技、军事、旅游、国际、证券、农业、电竞|im_end| |im_start|user 新闻标题上课时学生手机响个不停老师一怒之下把手机摔了家长拿发票让老师赔大家怎么看待这种事 类别|im_end| |im_start|assistant 教育|im_end|参与计算loss的教育|im_end|5.1.2 效果与资源消耗用5090显卡全量微调Qwen2-0.5B模型训练三轮python train_sft.py --full_ft --lr 2e-5 --num_train -1显存占用约12.5GB平均一轮950s LLM SFT 分类结果 样本数 : 10000 准确率 : 5656/10000 0.5656 无法解析 : 0 条 (0.0%) 总耗时 : 255.9s均值 0.03s/条5.2 LoRA微调5.2.1 LoRA是什么LoRALow-Rank Adaptation低秩适配是一种参数高效微调技术。它的核心思想很简单不修改原模型只在旁边挂两个小矩阵来学习新任务。全量微调要训练d*k的参数lora只用训练d*r r*k的参数。公式是h W·x (α/r) · B·A·xLoRA的优势有以下几点实证发现微调时权重更新量 ΔW 的有效秩极低r8 已能逼近全量效果参数量大幅降低。7B 模型从 70 亿可训参数 → ~4M仅 0.06%显存从 80G 降到 16G原始预训练权重 W 完全冻结模型常识不会被任务数据破坏。多任务零成本切换 — base 模型只需一份不同任务存独立的参数按需加载。5.2.2 核心代码与关键参数lora_configLoraConfig(task_typeTaskType.CAUSAL_LM,r8,# 秩两个小矩阵的中间维度lora_alpha16,# 缩放因子控制 LoRA 对输出的影响强度target_modules[q_proj,k_proj,v_proj,o_proj],# 在注意力层的四个投影矩阵上挂 LoRAlora_dropout0.05,# 防止小矩阵过拟合biasnone,# 不训练偏置项)modelget_peft_model(model,lora_config)# 冻结原模型注入 LoRA 小矩阵5.2.3 效果训练三轮python train_sft.py --num_train -1显存占用约12GB平均一轮约1100s。 LLM SFT 分类结果 样本数 : 10000 准确率 : 5828/10000 0.5828 无法解析 : 3 条 (0.0%) 总耗时 : 248.9s均值 0.02s/条5.3 Zero-Shot 结果 Zero-Shot LLM 分类结果Qwen2-0.5B-Instruct 样本数 : 10000 准确率 : 3345/10000 0.3345 无法解析 : 2664 条 (26.6%) 总耗时 : 285.5s, 均值 0.03s/条6. 综合对比与总结6.1 三方案横向评测表维度DeepSeek APIBert 微调Qwen 0.5B Zero-ShotQwen 0.5B 全量Qwen 0.5B LoRA模型大小—102M495M495M495M(1M)训练数据量053K053K53K训练时间—90s/epoch—950s/epoch1100s/epoch训练占用显存—3.3GB—12.5GB12GB准确率53.0%56.2%33%56.6%58.2%无法解析率1.5%0%26.6%0%0.03%单条推理用时840ms0.8ms29ms26ms25ms推理 1 万条总耗时140min7.8s285.5s255.9s248.9s6.2 场景化选型建议场景推荐方案原因标注数据充足500条以上Bert 微调效果稳定推理极快0% 解析失败标注数据极少或为零LLM 方案Bert 需要标注数据才能微调LLM 可 zero-shot 或 few-shot追求输出稳定可控不容许格式错误Bert 微调输出 logits 直接取 argmax100% 可控批量推理量大延迟要求高Bert 微调单条 0.8ms1 万条 7.8 秒分类体系频繁变化API改 prompt 就行不用重新训模型多个分类任务共存共享基座LoRA一个基座 多个几 MB 的 adapter分类结果需要附带解释LLM 方案生成式模型天然支持Bert 做不到