实现意图识别时,可以先比较三种基础方法:关键词规则、TF-IDF 加线性分类器、句向量近邻。它们分别依赖显式条件、业务标注和预训练语义表示,可以作为后续优化的基线。
本文用同一组中文酒店教学数据运行三个识别器,公开数据划分、实现参数、阈值选择和逐条预测。通过对比,我们会看到三种方法为什么会判断不同,以及怎样根据错误选择下一步优化。
下载代码、数据和完整实验结果。标签定义见意图标签设计,复杂多轮样例见48 条语料专题。
一、任务定义与数据准备
本次只识别单轮句子的业务类别,不提取槽位、不恢复历史,也不执行工具。使用 7 个标签:政策咨询、查房、预订、订单查询、修改预订、取消预订、退款咨询。
为比较三种方法,准备三份用途不同的数据:
| 文件 | 组成 | 用途 |
|---|---|---|
| train.jsonl | 42 条,7 类各 6 条 | 拟合 TF-IDF 与分类器,建立向量示例库 |
| validation.jsonl | 21 条已知类别 + 7 条范围外 | 选择分数阈值和候选分差 |
| probe.jsonl | 14 条已知类别 + 7 条范围外 | 参数固定后的演示检查 |
下文用这组教学数据解释方法差异。完整数据与运行环境见下载包 README。
训练集用来学习,验证集用来选阈值,检查集用来观察固定方案的行为。将这三种用途分开,才能知道改善来自哪里。
二、统一识别器接口
三个识别器都实现 score(texts),返回每句输入对应的标签到分数的映射。它们的分数含义不同:规则输出是否命中,TF-IDF 分类器输出概率,句向量输出与该类示例的最高余弦相似度。
这些方法使用相同的返回结构,但分数的含义和取值尺度不同。不能因为两个方案都返回 0.7,就认为可靠程度相同。最终选不选标签,由各自的验证数据与决策策略决定。
候选接口也不直接替代第一批的完整结构化输出。缺少槽位与上下文的分类器,不应返回未经提取或推断的字段值。后续路由可以先使用候选,再调用参数提取或对话状态模块。
三、关键词与正则规则
规则实现很简单:每个类别有若干正则表达式,只要命中其中一个,就返回 1,否则返回 0。例如“查.*订单”用于订单查询,“退款”用于退款咨询。
规则通过文本模式选择分类分支。它很容易解释,也容易执行,但模式本身通常不知道否定修饰哪个动作、条件约束哪个目标。
“不要取消,只查订单”可能同时命中两个类别。本文在并列最高分时选择澄清,而不是由字典顺序决定最终动作。没有命中时也不默认返回第一个标签。
生产规则可以更细,但要监控规则互相遮蔽、顺序变化和新增例外的影响。增加规则后仍需测试,确认是否提高了覆盖率、是否引入新的冲突。
四、TF-IDF 与逻辑回归分类
中文词语之间通常没有空格,本例直接使用字符 n-gram 提取特征。本例使用 2 到 4 个字符的连续片段,并在训练数据上拟合词表、IDF 和逻辑回归分类器:
import json
from pathlib import Path
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
train = [json.loads(line) for line in Path("batch2/data/train.jsonl").read_text().splitlines()]
model = make_pipeline(
TfidfVectorizer(analyzer="char", ngram_range=(2, 4), min_df=1),
LogisticRegression(C=4.0, class_weight="balanced", max_iter=1000, random_state=42),
)
model.fit([row["text"] for row in train], [row["label"] for row in train])
print(dict(zip(model.classes_, model.predict_proba(["查一下酒店订单"])[0])))
字符片段可以捕捉“退款”“到账”“订单”等线索,也可能学习到模板、订单号前缀等偶然特征。真实实验应检查脱敏方式和实体替换后的表现,检查模型是否依赖了订单号格式等无关特征。
这里的 TF-IDF 把文本转成稀疏特征,逻辑回归学习每个特征对不同标签的影响。TfidfVectorizer 文档
TF-IDF 可以拆成两个量理解。TF 描述一个片段在当前文本中的出现情况,IDF 降低到处都出现的片段权重。若“酒店”几乎每个类别都出现,它提供的区分信息很少;“到账”集中出现在退款类中,往往更有帮助。
逻辑回归再对这些特征加权:每个类别计算 z_k = w_k · x + b_k,多类别模型把这些分数通过 softmax 转成分布。训练时根据标注修正权重,使正确类别获得更高分。它与手写规则的差别在于,特征对类别的正负作用由数据学习,而不完全依靠开发者逐条规定。
字符 n-gram 也解释了它的弱点。“退回款项”和“钱什么时候到账”可能缺少相同片段;同义表达跨度大时,稀疏特征很难自行建立关联。把 n-gram 拉长可以保留局部语序,却会增加稀疏性;太短又容易失去短语意义。
模型的 predict_proba 输出也不能自动解决范围外输入。已知类别上的概率分布仍然总和为 1,一个完全无关的请求也会有最大类别。需要另加拒识实验,而不是直接最大值路由。
五、句向量与近邻分类
本例加载多语言句向量模型 sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2,在 CPU 上运行,并固定模型仓库 revision。训练例句与查询都做向量归一化,然后用点积计算余弦相似度。
该模型输出 384 维表示,让每句话变成一个固定长度的向量。模型卡
训练例句 → 编码、归一化 → 示例向量矩阵
当前输入 → 编码、归一化 → 与示例计算相似度
↓
同一类别取最高示例分数
↓
分数阈值与候选分差判断
Sentence Transformers 提供句子编码与相似度接口,具体比较方式与归一化配置应保持一致。相似度文档
归一化后的向量长度为 1,点积就等于余弦相似度。模型将语义相近的表达映射到相近方向,因此“钱何时回来”和“退款多久到账”即使字面重合较少,也可能靠近。这里的关键能力来自编码器预训练,而不来自当前 42 条例句的训练。
但语义相近不等于意图相同。“取消订单”和“取消订单会收费吗”主题非常接近,却应进入操作与咨询两个分支。这解释了为什么向量方法需要边界反例,也解释了通用语义相似度不能直接等同于业务决策。
示例级检索和类别中心各有代价。类别中心把同类向量平均后比较,存储与检索更简单,却可能丢失同类样例中不同表达的特征;示例级检索保留多样性,但更容易被单条错误标注或偶然相似的例子影响。本文实际运行的是“每类最高示例相似度”,类别中心方案未参与本次比较。
模型输入长度也是限制。把长历史直接拼接到句向量编码器,可能截断关键上下文;本实验只处理短句,不据此推断多轮能力。
六、选择拒识阈值
每个方法分别使用验证集搜索最高分阈值和前两名分差。教学目标函数为“5 倍错误接收 + 1 倍已知输入未选择”,优先减少错误接收,但接受一定的拒识代价。
规则命中、分类概率和余弦相似度使用同一候选网格只是便于演示,最终各自选择参数。阈值不是通用常数,标签、示例和模型变化后都需要重新检查。
更完整的解释见未知意图、拒识与澄清。拒识表示当前证据不足以选择,不等于已经确定用户在业务范围外。
七、分类结果与错误分析
| 方法 | 已知输入原始 Top-1 | 接收数 / 21 | 接收错误数 | 范围外误接收 / 7 |
|---|---|---|---|---|
| 关键词规则 | 8/14 | 10/21 | 3 | 1/7 |
| TF-IDF + 逻辑回归 | 9/14 | 6/21 | 1 | 1/7 |
| 句向量近邻 | 10/14 | 4/21 | 0 | 0/7 |
原始 Top-1 只看 14 条已知输入,不包含范围外样本,也不使用拒识策略。它有助于分离“候选排序能力”和“是否接收的决策”。规则零命中时的原始排序会按标签顺序打破平局,这一结果仅用作不含拒识策略的分类参照,实际策略不会接收零命中或并列最高分。
阈值后的接收数以全部 21 条 probe 输入为分母;接收错误包含已知类误判和范围外误接收。因此要同时观察排序与拒识:前者回答“最像哪个类别”,后者回答“现在是否足以作出选择”。
下面三句分别对应否定表达、已有订单修改和范围外请求中的识别错误:
| 输入 | 正确类别 | 规则决策 | TF-IDF 决策 | 句向量决策 |
|---|---|---|---|---|
| 别取消了,把 DEMO-Z 的订单详情给我看看 | 查订单 | 取消预订 | 需要区分候选 | 拒识 |
| DEMO-Z 这张订单换个房型 | 修改预订 | 查房 | 需要区分候选 | 拒识 |
| 明天的足球比赛几点开始 | 范围外 | 政策咨询 | 政策咨询 | 拒识 |
第一句需要理解“别”否定的是取消;第二句里的“换房型”发生在已有订单上,应进入修改流程;第三句虽然包含“几点”,却谈论足球比赛。它们说明:关键词、局部字符和语义相似度各自提供一部分证据,业务目标还取决于否定范围、任务阶段和能力目录。
除了总分,还应分析具体错例。如果取消政策被识别为取消动作,要检查标签正反例和语义边界;如果开票被识别为订单查询,要检查范围外样本;如果换一种说法就识别失败,应检查文本特征和训练样本是否覆盖这类表达。
八、推理耗时对比
| 方法 | 单条推理中位数 | 本次样本 P95 |
|---|---|---|
| 关键词规则 | 0.005 ms | 0.005 ms |
| TF-IDF + 逻辑回归 | 0.100 ms | 0.203 ms |
| 句向量近邻 | 4.542 ms | 4.936 ms |
计时采用 CPU 预热后逐条推理,排除模型加载;完整环境在结果文件中。规则只做模式匹配,TF-IDF 需要稀疏特征与线性运算,句向量还要执行神经网络编码,这些计算路径解释了延迟差异。
线上成本还取决于调用方式。固定示例的向量可以预先计算;多个输入可批量编码;高频、确定的规则入口可以减少模型调用。优化之前先区分初始化、单次推理和排队时间,才能找到真正的瓶颈。
九、根据业务需求选择方法
可以根据任务约束安排实验,而不是先决定必须使用某类模型。
固定菜单、少量明确命令可以先用规则。标签稳定且已有标注时,TF-IDF 加线性分类器能提供成本较低的比较起点。希望通过增加例句快速覆盖同义表达时,可以尝试向量近邻,但要专门检查否定与范围外输入。需要复杂上下文解释时,再比较大模型或适合的专用模型。
希望进一步理解细粒度意图分类,可以阅读 BANKING77 相关研究,了解如何通过双编码器表示例句与区分相近类别。BANKING77 论文
小模型微调、蒸馏和线上分层路由需要更多数据与实验,本篇先建立三个可运行基线。采用更复杂的方法前,应先明确要解决哪些已发现的问题,再用实验验证效果。
十、运行实验代码
在下载包根目录创建隔离环境:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r batch2/requirements.txt
python batch2/run_experiment.py
python batch2/tests.py
首次运行句向量方案需要从模型仓库下载权重。模型 ID 与 revision 保存在 batch2/model.json,库版本与原始结果在 batch2/results/experiment.json。下载包只包含代码、数据、配置与实验结果,不重新分发模型权重。
只运行规则与 TF-IDF,可使用 python batch2/run_experiment.py --without-embeddings;当前完整 requirements 仍安装实验依赖,读者可以在确认依赖后另建最小环境。离线策略与任务图测试只用标准库。
保留基线的标签定义、训练数据、分数、拒识结果和错例,便于比较后续改动。补充数据或更换模型后,可以据此检查哪些错误得到修复,哪些类别的表现有所下降。