AB
AiBoss站
教程

Jev

教程

从零构建 Jev 式快速决策引擎:用 Qwen 开源模型改造分类头

Jev 是一类把大模型的理解能力压缩成单次前向判断的决策引擎,输出结构化结果而非整段文字。本文说明它解决什么问题,并给出用 Qwen2.5-Coder-1.5B-Instruct 作为骨干、替换分类头、准备数据并训练出本地快速分类器的完整流程。

传统程序依赖人写死的 if-then 规则,遇到边界模糊的输入就会失效;而聊天式大模型虽然能理解语境,却要逐词生成整段回答,速度慢、输出不稳定,程序还得从自然语言里再抠出结论。Jev 这类模型走的是另一条路:它保留大模型对语言的理解能力,但把「逐词生成」的输出头换成「分类」输出头,一次前向传播直接给出结构化判断,例如 [CATEGORY: LETTER, CONFIDENCE: 98%]。它适合需要在程序里做高速分流、打标、判定的开发者,例如先把客服消息分成技术问题、账单问题或垃圾信息,再决定交给哪个更慢更贵的下游流程。站内也有对应的工具条目可以参考:Jev。本文讲的是如何用开源权重模型自己搭一个同类引擎。

准备工作

在动手之前,需要先确认几件事。

  • 硬件:本文选用的是 1.5B 量级的轻量模型,目标是能在普通电脑上跑起来,不需要数据中心级显卡。具体显存与内存占用取决于精度和批次大小,请以实际运行情况为准。
  • Python 环境:需要一个可用的 Python 环境,并能安装 PyTorch、Transformers 等常规依赖。
  • 模型下载:首次运行会从 Hugging Face 拉取权重文件,需要网络可达;之后会复用本地缓存,不再重复下载。
  • 数据:需要自己准备一份带标签的文本样本集,标签就是你想让引擎输出的类别。

本文的示例项目由三个脚本组成:model.py、train.py 和 check_names.py。依赖通过 requirements 文件统一安装:

pip install -r requirements.txt

关于模型版本、许可证、可用地区以及各家平台的配额与价格,请以官网当前信息为准,本文不逐一列举。

操作步骤

第一步:理解为什么要换掉输出头

主流聊天模型本质上是高级自动补全。给它一段提示,它计算下一个最可能的词元并输出,然后把原提示加上新词元再算下一个,如此循环,直到生成停止词。这个过程叫自回归生成。它的代价是:哪怕你只想知道「这封邮件是不是垃圾邮件」,模型也要把整句话一个词一个词拼出来,程序再去解析这句话。

从结构上看,一个模型可以拆成两部分:骨干和头。骨干是底层的大网络,负责理解语言、语境和细微差别;头则负责把这份理解转成最终输出。聊天模型的头是为逐词生成设计的,但头是可替换的。把生成头摘掉、换上一个分类头,骨干的理解能力原封不动,输出方式却从「循环多次」变成「单次通过」——这正是速度差异的来源。

第二步:加载骨干,不要生成头

先选一个开源骨干作为地基。本文使用 Qwen2.5-Coder-1.5B-Instruct:体积小,且本身已经理解代码,便于后面的演示。分词器负责把你的文本切成模型认识的小块;AutoModel 只请求骨干本身,下载包里虽然带着生成头,但这次调用不会把它取出来。

from transformers import AutoModel, AutoTokenizer

model_id = "Qwen/Qwen2.5-Coder-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
backbone = AutoModel.from_pretrained(model_id)

第一次运行会从 Hugging Face 下载文件,之后复用本机已有的副本。

第三步:接上一个空的分类头

接下来做「换头」。骨干参数被冻结——requires_grad = False 表示这些数值不允许再变;self.score 是新的头,输出两个分数,因为本示例只有两个类别:match 和 mismatch。头的宽度与骨干的隐藏维度对齐,两块才能接上。

import torch
from torch import nn

class JevModel(nn.Module):
    def __init__(self, backbone):
        super().__init__()
        self.backbone = backbone
        for parameter in self.backbone.parameters():
            parameter.requires_grad = False
        self.score = nn.Linear(backbone.config.hidden_size, 2)

    def forward(self, input_ids, attention_mask):
        with torch.no_grad():
            hidden = self.backbone(
                input_ids=input_ids,
                attention_mask=attention_mask,
            ).last_hidden_state
        last = attention_mask.sum(dim=1) - 1
        pooled = hidden[torch.arange(hidden.size(0)), last]
        return self.score(pooled.float())

这里有几个关键点需要说清楚。

  • 单次通过:骨干从左到右读完整段文本,读到最后一个真实词元时,它已经看过全部内容,所以头只需要看这个位置,直接吐出两个分数。
  • 不记梯度:torch.no_grad() 告诉计算机不要为改动骨干而保留中间记录,只有那个小小的头允许学习。
  • 不写任何词:整个前向过程不生成一个词,输出就是分数。
  • 取最后有效位置:attention_mask.sum(dim=1) - 1 算出每条样本最后一个真实词元的下标,用它做池化,避免把填充位置当成内容。

第四步:准备训练数据

骨干虽然已经理解语言,但新接上的头完全没训练过,它还不知道怎么把这份理解映射到你的类别上。要补上这一步,就得给模型一批清晰的样例,也就是训练数据集。比如你想让引擎给邮件分流,就准备成千上万条「文本 + 正确类别」的配对,标出哪些是垃圾邮件、哪些是紧急问题。训练过程中,庞大的骨干基本保持冻结不变,只有那个小头在更新。

数据准备上有几点值得注意:

  • 类别要固定:分类头的输出维度在初始化时就定死了。本示例是 2 类,如果你要分 5 类,就要把 nn.Linear 的第二个参数改成 5,并保证标签编号与之一致。
  • 标签要一致:同一类别的文本必须用同一个标签,否则头学到的映射会互相冲突。
  • 样本要覆盖边界情况:规则程序最容易在奇怪输入上翻车,训练集里也应该包含这类难例,否则引擎在真实数据上同样会失手。
  • 文本长度:超长文本会被截断,截断策略要与你的实际输入分布匹配。

第五步:训练并保存

训练脚本要做的事情是:加载骨干、套上分类头、把文本转成词元、前向计算分数、与标签比对算损失、反向传播更新头的参数。由于骨干被冻结,需要更新的参数量很小,训练开销主要花在前向计算上。

训练完成后,把头的权重单独保存下来即可。骨干可以从原始模型重新加载,头则从你保存的文件恢复,这样整个引擎的体积增长很小。

第六步:推理与验证

推理时不再需要标签,只需要把文本喂进去,取分数最高的类别作为输出。示例项目里的 check_names.py 就是用来做这类验证的脚本:给定若干条输入,打印模型给出的判定,人工核对是否符合预期。

验证时建议关注两类情况:一类是明显应该判对的样本,用来确认流程通了;另一类是刻意构造的模糊样本,用来观察模型在边界上的表现。如果边界样本错得离谱,通常说明训练数据里缺少同类样本,而不是架构本身有问题。

一个完整示例

下面把前面的步骤串成一个最小可运行的流程。假设我们要判断一段代码片段与某个函数名是否匹配,类别为 match 和 mismatch。

第一步,安装依赖。

pip install -r requirements.txt

第二步,写 model.py,内容就是前面给出的 JevModel 定义:加载骨干、冻结参数、接上两维分类头、在最后一个有效位置池化后输出分数。

第三步,准备数据。构造一个列表,每项包含一段文本和一个标签(0 表示 mismatch,1 表示 match)。文本可以是「函数名 + 代码片段」的拼接形式,标签表示两者是否对应。

第四步,写 train.py。流程如下:

  1. 用 AutoTokenizer.from_pretrained(model_id) 加载分词器。
  2. 用 AutoModel.from_pretrained(model_id) 加载骨干。
  3. 用骨干实例化 JevModel。
  4. 把文本批量编码为 input_ids 与 attention_mask。
  5. 前向得到两个分数,用交叉熵损失与标签比对。
  6. 反向传播,只更新 self.score 的参数。
  7. 多轮迭代后保存头的权重。

第五步,写 check_names.py。加载骨干与保存好的头,对若干条输入做前向,打印每条输入对应的预测类别与分数。

第六步,运行验证。先跑训练脚本,再跑检查脚本。如果输出中明显匹配的样本被判为 match、明显不匹配的被判为 mismatch,说明整条链路已经打通。之后就可以把这个引擎接到你自己的程序里,作为高速分流的第一道判断。

注意事项

  • 骨干冻结是设计的一部分:训练时只更新分类头,不要因为效果不理想就贸然解冻骨干。解冻会显著增加显存占用和训练时间,也容易在小数据集上过拟合。
  • 类别数在初始化时确定:分类头的输出维度写死在 nn.Linear 的第二个参数里。要改类别数,必须重新初始化并重新训练,不能只改标签。
  • 池化位置依赖 attention_mask:如果掩码处理有误,取到的可能是填充位置,输出会变得没有意义。批量推理时尤其要检查这一点。
  • 首次下载需要网络:模型权重从 Hugging Face 拉取,离线环境下需要提前准备好本地缓存。
  • 输出是分数不是文字:引擎不会给你一句解释,只给每个类别的分数。需要可读解释的场景,要在下游另行处理。
  • 训练数据决定上限:头是空白的,它的全部知识来自你给的样例。样例覆盖不到的情况,引擎同样判断不了。
  • 版本与配额:模型版本、许可证、可用地区、平台配额与价格都可能变化,请以官网当前信息为准。