揭秘AI原生应用领域意图识别的核心技术
传统意图识别和AI原生意图识别。自注意力机制的目标是:对于输入序列中的每个token,计算它与其他所有token的「相关性」,然后根据相关性加权求和,得到该token的「上下文向量」。比如输入序列是「订 明天 去 上海 的 机票」,对于token「订」,自注意力机制会计算它与「明天」「上海」「机票」的相关性,然后加权求和得到「订」的上下文向量——这个向量包含了「订」与其他token的关系(比如「订
揭秘AI原生应用领域意图识别的核心技术
当你对着Copilot说「帮我优化这段Python代码的性能」,它立刻明白你需要「代码优化」服务;当你在ChatGPT中说「帮我查明天北京到上海的高铁票」,它会自动调用12306插件并传递关键参数——这一切的背后,都是**意图识别(Intent Recognition)**在发挥作用。
AI原生应用(AI-Native Application)的核心竞争力,在于「理解用户需求」的能力。而意图识别,就是AI理解用户的「第一扇门」——它将用户的自然语言、语音甚至多模态输入,转化为机器可执行的「意图指令」,并提取关键参数(槽位)。没有精准的意图识别,再强大的大模型也会沦为「对牛弹琴」的工具。
一、什么是AI原生应用的意图识别?
在聊技术之前,我们需要先明确两个概念:传统意图识别和AI原生意图识别。
1. 传统意图识别:封闭域、规则驱动
传统的意图识别(比如早期的Siri、小度),本质是「封闭域的规则匹配」。开发者会预先定义一个「意图列表」(比如「查询天气」「播放音乐」「设置闹钟」),然后为每个意图编写规则(比如关键词匹配:包含「天气」则触发「查询天气」意图),或者用简单的机器学习模型(比如SVM、随机森林)基于关键词特征分类。
这种方式的问题很明显:
- 无法处理开放式意图:如果用户说「帮我找一家附近的猫咖」,而预定义意图中没有「寻找猫咖」,系统就会报错;
- 缺乏上下文理解:如果用户先问「最近有什么好电影」,再问「它的评分怎么样」,传统系统无法理解「它」指的是「最近的好电影」;
- 对自然语言的灵活性支持差:比如用户说「明天去上海的机票有没有」和「有没有明天到上海的机票」,虽然语义相同,但规则可能无法覆盖所有句式。
2. AI原生意图识别:开放域、上下文驱动、大模型赋能
AI原生应用的意图识别,是「以大模型为核心,支持开放域、上下文感知、多模态」的意图理解系统。它的核心特点包括:
- 开放域处理:不需要预定义所有意图,能通过Few-shot/Zero-shot学习识别未见过的意图;
- 上下文感知:能理解多轮对话中的指代、意图延续(比如「订咖啡→加蛋糕」);
- 多模态支持:能处理文本、语音、图像、视频等多模态输入的意图(比如「这张图片里的猫可爱吗?」);
- 槽位与意图联合优化:不仅识别意图,还能提取意图的关键参数(槽位),并通过联合建模提高准确率。
简单来说,传统意图识别是「听关键词」,而AI原生意图识别是「听懂意图」——它能理解用户的「言外之意」,甚至「未说之言」。
二、意图识别的基础框架
要理解AI原生意图识别的技术,我们需要先看它的基础框架。下图是一个典型的AI原生意图识别流程:
1. 用户输入:多模态兼容
AI原生应用的用户输入不再局限于文本,还包括语音(比如语音助手)、图像(比如AI识图)、视频(比如视频内容理解)。比如用户上传一张「猫的图片」并说「它多大了?」,输入就是「图像+文本」的多模态数据。
2. 多模态预处理
预处理的目标是将原始输入转化为模型可处理的格式:
- 文本预处理:分词(比如用Jieba分词处理中文)、去除停用词(比如「的」「了」)、实体识别(比如识别「北京」「上海」为地名);
- 语音预处理:将语音信号转化为梅尔频谱图(Mel Spectrogram),然后用语音模型(比如Wav2Vec 2.0)转化为文本;
- 图像预处理:将图像 resize 到模型要求的尺寸(比如224x224)、归一化(比如减去均值除以标准差),然后用图像模型(比如ResNet)提取特征。
3. 上下文融合
上下文融合是AI原生意图识别的关键步骤之一。它将「当前用户输入」与「历史对话/交互数据」结合,解决指代消解、意图延续的问题。比如:
- 历史对话:「用户:我想订明天去上海的机票。系统:请问出发地是哪里?用户:北京。」——历史对话中的「订明天去上海的机票」是当前输入「北京」的上下文;
- 交互数据:比如用户之前的购买记录(比如经常订早上的机票),可以辅助意图识别(比如用户说「订机票」,系统优先推荐早上的航班)。
常用的上下文融合方法包括:
- 循环神经网络(RNN):用RNN的隐藏状态保存历史信息;
- Transformer的Memory机制:将历史对话编码为Memory向量,与当前输入向量拼接;
- 大模型的上下文窗口:比如GPT-4的8k/32k上下文窗口,直接将历史对话作为输入的一部分。
4. 特征提取与编码
特征提取的目标是将预处理后的输入转化为「语义向量」——机器能理解的「语言」。在AI原生意图识别中,**预训练语言模型(PLM)**是绝对的核心,比如:
- 文本编码:BERT、RoBERTa、GPT(用于文本输入);
- 多模态编码:CLIP(用于文本+图像输入)、Whisper(用于语音输入)。
以BERT为例,它通过「双向Transformer」将文本转化为「上下文相关的语义向量」。比如「订明天去上海的机票」中的「订」「明天」「上海」「机票」都会被编码为包含上下文信息的向量——「订」的向量会包含「与机票相关的动作」的信息,「上海」的向量会包含「目的地」的信息。
5. 意图分类
意图分类是将编码后的语义向量映射到「意图标签」的过程。在AI原生应用中,意图分类可以是:
- 封闭域分类:预定义意图列表(比如「预订机票」「查询天气」),用全连接层+Softmax输出每个意图的概率;
- 开放域生成:用大模型的文本生成能力,直接输出意图描述(比如用户输入「帮我找附近的猫咖」,模型输出「意图:寻找猫咖」)。
6. 槽位填充
槽位填充是提取意图的「关键参数」的过程,比如「预订机票」的槽位包括「出发地」「目的地」「时间」「舱位等级」。槽位填充本质是「序列标注任务」——给每个token打上槽位标签(比如「北京」→「出发地」,「上海」→「目的地」,「明天」→「时间」)。
常用的槽位填充模型包括:
- CRF(条件随机场):处理序列标注的经典模型,能考虑相邻token的标签依赖(比如「出发地」后面不太可能跟着「时间」);
- Transformer+CRF:用Transformer提取特征,CRF优化序列标签,是当前槽位填充的主流方法。
7. 应用逻辑执行
最后,意图输出和槽位输出会被传递给应用逻辑层,执行具体的操作:比如调用机票预订API、查询天气API、生成代码等。
三、核心技术点拆解
AI原生意图识别的核心竞争力,在于解决「传统意图识别无法处理的问题」。接下来,我们深入拆解三个关键技术点:开放式意图识别、上下文感知的意图推理、槽位与意图的联合建模。
一、开放式意图识别:从“封闭列表”到“开放学习”
传统意图识别的最大痛点,是无法处理「未预定义的意图」。比如用户说「帮我找一家能撸猫的咖啡馆」,如果预定义意图中没有「寻找猫咖」,系统就会回复「我听不懂你的问题」——这在AI原生应用中是无法接受的,因为AI原生应用需要支持「无限可能的用户需求」。
开放式意图识别的目标,是让模型「在少量示例或无示例的情况下,识别未见过的意图」。核心技术包括:Few-shot Learning(少样本学习)、Zero-shot Learning(零样本学习)、意图聚类(Intent Clustering)。
1. Few-shot Learning:用少量示例“学会”新意图
Few-shot Learning的核心思想是:「给模型看几个例子,它就能识别新的意图」。比如要让模型识别「寻找猫咖」的意图,只需要给它3-5个示例:
- 输入:「帮我找一家附近的猫咖」→ 意图:寻找猫咖
- 输入:「有没有能撸猫的咖啡馆?」→ 意图:寻找猫咖
- 输入:「我想找个有猫的地方喝咖啡」→ 意图:寻找猫咖
模型通过学习这些示例的「语义特征」,就能识别新的「寻找猫咖」意图。
在大模型时代,Few-shot Learning的实现方式主要是Prompt Engineering(提示工程)——将意图识别转化为「文本生成任务」。比如:
- 输入Prompt:「用户输入:帮我找一家附近的猫咖。意图:寻找猫咖;用户输入:有没有能撸猫的咖啡馆?意图:寻找猫咖;用户输入:我想找个有猫的地方喝咖啡。意图:寻找猫咖;用户输入:帮我找一家能撸猫的店。意图:」
- 模型输出:「寻找猫咖」
Prompt Engineering的关键是「设计有效的提示模板」,让模型理解「输入→意图」的映射关系。常用的Prompt模板包括:
- 前缀Prompt:在输入前添加示例,比如「例子:输入→意图;输入→意图;当前输入→意图:」;
- 指令Prompt:用自然语言指令告诉模型任务,比如「请识别用户输入的意图,意图标签包括:寻找猫咖、预订机票、查询天气。用户输入:帮我找一家附近的猫咖。意图:」。
2. Zero-shot Learning:无示例识别新意图
Zero-shot Learning比Few-shot更激进:「不需要任何示例,模型就能识别新意图」。它的核心思想是「利用意图的「语义描述」来识别新意图」。比如:
- 意图的语义描述:「寻找猫咖」=「寻找提供猫咪陪伴的咖啡馆」;
- 用户输入:「帮我找一家能撸猫的咖啡馆」;
- 模型通过比较「用户输入的语义」与「意图语义描述的语义」,判断意图是「寻找猫咖」。
在大模型中,Zero-shot Learning的实现方式是将意图的语义描述作为Prompt的一部分。比如:
- 输入Prompt:「请识别用户输入的意图。意图列表及描述:1. 寻找猫咖:寻找提供猫咪陪伴的咖啡馆;2. 预订机票:预订往返或单程的飞机票;3. 查询天气:查询某个城市的天气情况。用户输入:帮我找一家能撸猫的咖啡馆。意图:」
- 模型输出:「寻找猫咖」
3. 意图聚类:发现“未知的意图”
除了识别「已知的新意图」,开放式意图识别还需要「发现未知的意图」——比如用户输入的内容不在任何预定义意图或语义描述中,模型需要自动聚类这些输入,发现新的意图类别。
常用的意图聚类方法是无监督学习,比如:
- DBSCAN:基于密度的聚类算法,能发现任意形状的聚类(比如「寻找猫咖」「寻找狗咖」是两个不同的聚类);
- K-means:基于距离的聚类算法,需要预先指定聚类数量(比如K=5,将输入分为5个意图类别);
- 层次聚类:将输入逐步合并为更大的聚类,能生成聚类的层次结构(比如「寻找宠物咖啡馆」→「寻找猫咖」「寻找狗咖」)。
意图聚类的流程通常是:
- 用预训练模型(比如BERT)将用户输入编码为语义向量;
- 对语义向量进行聚类(比如DBSCAN);
- 人工标注聚类的意图标签(比如将某个聚类标注为「寻找猫咖」);
- 将新的意图标签添加到模型的意图列表中,更新模型。
二、上下文感知的意图推理:从“单轮”到“多轮”
在AI原生应用中,用户的需求往往是「多轮的」——比如用户先问「最近有什么好电影」,再问「它的评分怎么样」,再问「在哪里能看」。这时候,系统需要理解「它」指的是「最近的好电影」,「在哪里能看」是「查询电影播放平台」的意图,而不是「查询某个地点的位置」。
上下文感知的意图推理,就是让模型「记住历史对话内容,并利用历史内容理解当前意图」。核心技术包括:Memory-augmented Transformer(记忆增强Transformer)、指代消解(Coreference Resolution)、意图延续检测(Intent Carryover Detection)。
1. Memory-augmented Transformer:给模型“装个记忆库”
Transformer模型本身具有「上下文窗口」(比如GPT-4的8k窗口),能处理一定长度的历史对话。但对于更长的对话(比如10轮以上),Transformer的上下文窗口就不够用了——这时候需要Memory-augmented Transformer,给模型添加一个「外部记忆库」,用于存储历史对话的关键信息。
Memory-augmented Transformer的结构如下:
- 记忆库(Memory Bank):存储历史对话的编码向量(比如每个历史轮次的语义向量);
- 注意力机制(Attention):当前输入的编码向量与记忆库中的向量计算注意力,提取与当前输入相关的历史信息;
- 融合层(Fusion Layer):将当前输入的向量与注意力加权后的记忆向量融合,得到「上下文感知的语义向量」。
数学公式表示:
假设当前输入的编码向量为xtx_txt,记忆库中的向量为M=[m1,m2,...,mt−1]M = [m_1, m_2,...,m_{t-1}]M=[m1,m2,...,mt−1](mim_imi是第i轮对话的编码向量),则:
- 计算注意力权重:ai=softmax(xt⋅miTd)a_i = softmax(\frac{x_t \cdot m_i^T}{\sqrt{d}})ai=softmax(dxt⋅miT)(d是向量维度);
- 计算记忆向量:mtmem=∑i=1t−1aimim_t^{mem} = \sum_{i=1}^{t-1} a_i m_imtmem=i=1∑t−1aimi;
- 融合向量:xtctx=[xt;mtmem]x_t^{ctx} = [x_t; m_t^{mem}]xtctx=[xt;mtmem]([;]表示向量拼接)。
这样,xtctxx_t^{ctx}xtctx就包含了当前输入和历史对话的信息,模型用这个向量进行意图识别和槽位填充。
2. 指代消解:解决“它”“那”的问题
指代消解是上下文感知的核心任务之一——它的目标是「确定代词(比如「它」「那」「这个」)所指代的实体」。比如:
- 对话历史:「用户:我想订明天去上海的机票。系统:请问出发地是哪里?用户:北京。」
- 当前输入:「它的起飞时间是什么时候?」
- 指代消解的结果:「它」→「明天从北京到上海的机票」。
在AI原生意图识别中,指代消解的实现方式主要是大模型的上下文理解能力——比如GPT-4能直接理解「它」指代的是历史对话中的「机票」。对于更复杂的指代(比如「那个红色的杯子」),可以结合实体识别(NER)和共指消解模型(Coreference Resolution Model),比如Hugging Face的coref
库。
3. 意图延续检测:判断“是否是同一意图的延续”
意图延续检测的目标是「判断当前输入是否是历史意图的延续」。比如:
- 历史意图:「预订机票」(用户说「我想订明天去上海的机票」);
- 当前输入:「加一份行李托运」;
- 意图延续检测的结果:「是」——当前意图是「预订机票」的延续(添加行李托运)。
如果检测结果是「是」,系统就会将当前输入的槽位(比如「行李托运」)添加到历史意图的槽位中;如果是「否」,系统就会识别新的意图。
意图延续检测的常用方法是分类模型:将「历史对话编码向量」与「当前输入编码向量」拼接,输入到全连接层+Softmax,输出「是/否」的概率。比如:
- 输入:[mt−1;xt][m_{t-1}; x_t][mt−1;xt](mt−1m_{t-1}mt−1是历史对话的编码向量,xtx_txt是当前输入的编码向量);
- 输出:P(延续∣mt−1,xt)P(延续|m_{t-1}, x_t)P(延续∣mt−1,xt)(延续的概率)。
三、槽位与意图的联合建模:从“分开做”到“一起做”
在传统意图识别中,意图分类和槽位填充是「分开进行」的:先识别意图,再根据意图填充槽位。比如:
- 识别用户输入「订明天去上海的机票」的意图是「预订机票」;
- 根据「预订机票」的槽位列表(出发地、目的地、时间),填充槽位:出发地=(未提及)、目的地=上海、时间=明天。
这种方式的问题是误差传递:如果意图识别错了(比如把「订机票」识别成「查询天气」),那么槽位填充肯定也会错。而AI原生意图识别采用联合建模的方式,将意图分类和槽位填充作为「多任务学习」,共享模型的编码器,同时优化两个任务的损失函数——这样能减少误差传递,提高整体准确率。
1. 联合建模的结构:BERT + 双任务头
当前最主流的联合建模结构是Joint BERT(由Google在2019年提出),它的结构如下:
- 编码器:BERT,用于提取输入的语义向量;
- 意图分类头:全连接层+Softmax,输入是BERT的[CLS]向量(代表整个输入的语义),输出意图的概率分布;
- 槽位填充头:全连接层+CRF,输入是BERT的每个token的向量,输出每个token的槽位标签。
Joint BERT的结构示意图:
graph TD
A[输入文本] --> B[BERT编码器]
B --> C[[CLS]向量]
B --> D[Token向量序列]
C --> E[意图分类头(全连接+Softmax)]
D --> F[槽位填充头(全连接+CRF)]
E --> G[意图输出]
F --> H[槽位输出]
2. 联合损失函数:同时优化两个任务
Joint BERT的损失函数是意图分类损失和槽位填充损失的加权和:
Ltotal=αLintent+(1−α)LslotL_{total} = \alpha L_{intent} + (1-\alpha) L_{slot}Ltotal=αLintent+(1−α)Lslot
其中:
- LintentL_{intent}Lintent:意图分类的交叉熵损失,计算[CLS]向量与意图标签的误差;
- LslotL_{slot}Lslot:槽位填充的CRF损失,计算Token向量序列与槽位标签序列的误差;
- α\alphaα:权重系数(通常取0.5,平衡两个任务的重要性)。
(1)意图分类的交叉熵损失
对于意图分类,假设意图标签的真实分布是yintenty_{intent}yintent(one-hot向量),模型输出的概率分布是y^intent\hat{y}_{intent}y^intent,则:
Lintent=−∑i=1Cyintent,ilogy^intent,iL_{intent} = -\sum_{i=1}^C y_{intent,i} \log \hat{y}_{intent,i}Lintent=−i=1∑Cyintent,ilogy^intent,i
其中C是意图的数量。
(2)槽位填充的CRF损失
对于槽位填充,假设槽位标签序列是yslot=[y1,y2,...,yT]y_{slot} = [y_1, y_2,...,y_T]yslot=[y1,y2,...,yT](T是Token的数量),模型输出的Token向量序列是h=[h1,h2,...,hT]h = [h_1, h_2,...,h_T]h=[h1,h2,...,hT],CRF层的参数是转移矩阵AAA(Ai,jA_{i,j}Ai,j表示从标签i转移到标签j的概率),则CRF的损失函数是:
Lslot=−logexp(score(yslot,h))∑y′∈Yexp(score(y′,h))L_{slot} = -\log \frac{\exp(\text{score}(y_{slot}, h))}{\sum_{y' \in \mathcal{Y}} \exp(\text{score}(y', h))}Lslot=−log∑y′∈Yexp(score(y′,h))exp(score(yslot,h))
其中score(y,h)\text{score}(y, h)score(y,h)是标签序列y的分数:
score(y,h)=∑t=1T(Wytht+byt)+∑t=2TAyt−1,yt\text{score}(y, h) = \sum_{t=1}^T (W_{y_t} h_t + b_{y_t}) + \sum_{t=2}^T A_{y_{t-1}, y_t}score(y,h)=t=1∑T(Wytht+byt)+t=2∑TAyt−1,yt
WytW_{y_t}Wyt是槽位标签yty_tyt的权重矩阵,bytb_{y_t}byt是偏置项。
3. 联合建模的优势
相对于分开建模,联合建模有三个明显的优势:
- 减少误差传递:意图分类和槽位填充共享编码器,两个任务的信息能互相辅助(比如槽位中的「上海」能辅助意图分类为「预订机票」);
- 提高数据利用率:同一批数据能同时用于两个任务的训练,减少数据标注的成本;
- 提升准确率:根据实验,Joint BERT在SNIPS数据集上的意图分类准确率比分开建模高2-3%,槽位填充的F1-score高3-5%。
四、数学模型与公式:从原理到实践
前面我们讲了很多技术点,现在来深入拆解其中的核心数学模型——这些模型是意图识别的「底层逻辑」,理解它们能帮你更深入地掌握意图识别的技术。
一、Transformer的自注意力机制:理解“上下文”的关键
Transformer是当前所有预训练语言模型的核心,而**自注意力机制(Self-Attention)**是Transformer的灵魂——它让模型能「关注输入中的关键部分」,从而理解上下文语义。
1. 自注意力的定义
自注意力机制的目标是:对于输入序列中的每个token,计算它与其他所有token的「相关性」,然后根据相关性加权求和,得到该token的「上下文向量」。
比如输入序列是「订 明天 去 上海 的 机票」,对于token「订」,自注意力机制会计算它与「明天」「上海」「机票」的相关性,然后加权求和得到「订」的上下文向量——这个向量包含了「订」与其他token的关系(比如「订」与「机票」的相关性最高)。
2. 自注意力的数学公式
自注意力机制的计算过程分为三步:
- 生成查询(Query)、键(Key)、值(Value)向量:对于每个token的输入向量xix_ixi,用三个可学习的权重矩阵WQW_QWQ、WKW_KWK、WVW_VWV生成Q、K、V向量:
qi=xiWQq_i = x_i W_Qqi=xiWQ
ki=xiWKk_i = x_i W_Kki=xiWK
vi=xiWVv_i = x_i W_Vvi=xiWV - 计算注意力权重:对于每个token的q向量,与所有token的k向量计算点积,然后用Softmax归一化,得到注意力权重ai,ja_{i,j}ai,j(表示第i个token与第j个token的相关性):
ai,j=exp(qi⋅kjT/dk)∑k=1nexp(qi⋅kkT/dk)a_{i,j} = \frac{\exp(q_i \cdot k_j^T / \sqrt{d_k})}{\sum_{k=1}^n \exp(q_i \cdot k_k^T / \sqrt{d_k})}ai,j=∑k=1nexp(qi⋅kkT/dk)exp(qi⋅kjT/dk)
其中dkd_kdk是k向量的维度,除以dk\sqrt{d_k}dk是为了避免点积结果过大,导致Softmax后的梯度消失。 - 计算上下文向量:用注意力权重ai,ja_{i,j}ai,j对所有token的v向量加权求和,得到第i个token的上下文向量ziz_izi:
zi=∑j=1nai,jvjz_i = \sum_{j=1}^n a_{i,j} v_jzi=j=1∑nai,jvj
3. 多头自注意力(Multi-Head Attention)
为了让模型能关注「不同类型的相关性」,Transformer采用了多头自注意力——将自注意力机制重复多次(比如8次、12次),每次生成不同的Q、K、V向量,然后将多个头的结果拼接,得到最终的上下文向量。
多头自注意力的数学公式:
MultiHead(Q,K,V)=Concat(head1,head2,...,headh)WOMultiHead(Q,K,V) = Concat(head_1, head_2,...,head_h) W_OMultiHead(Q,K,V)=Concat(head1,head2,...,headh)WO
其中headi=Attention(QWQi,KWKi,VWVi)head_i = Attention(Q W_{Q_i}, K W_{K_i}, V W_{V_i})headi=Attention(QWQi,KWKi,VWVi),hhh是头的数量,WOW_OWO是可学习的权重矩阵。
比如在BERT中,多头自注意力的头数是12,每个头的维度是64(总维度是768=12×64)——这样模型能同时关注「语法关系」(比如「订」与「机票」的动宾关系)、「语义关系」(比如「明天」与「时间」的关系)等不同类型的相关性。
二、Prompt Learning:让大模型“理解任务”的魔法
在大模型时代,Prompt Learning是「让大模型完成特定任务」的核心方法——它通过「设计提示」,将任务转化为大模型擅长的「文本生成任务」。对于意图识别来说,Prompt Learning能让大模型在Few-shot/Zero-shot的情况下,准确识别意图。
1. Prompt的结构
一个有效的Prompt通常包含三个部分:
- 指令(Instruction):用自然语言告诉大模型要完成的任务;
- 示例(Demonstration):给大模型看几个「输入→输出」的例子(Few-shot);
- 当前输入(Current Input):需要处理的用户输入。
比如意图识别的Prompt:
指令:请识别用户输入的意图,意图标签包括「预订机票」「查询天气」「寻找猫咖」。
示例:用户输入:帮我订明天去上海的机票→意图:预订机票;用户输入:北京明天的天气怎么样→意图:查询天气;用户输入:帮我找一家附近的猫咖→意图:寻找猫咖。
当前输入:帮我找一家能撸猫的咖啡馆→意图:
2. Prompt的数学原理
Prompt Learning的本质是「将任务转化为条件生成任务」——大模型根据Prompt中的指令、示例和当前输入,生成符合要求的输出。
对于意图识别任务,假设Prompt是PPP,当前输入是xxx,意图标签是yyy,则大模型的生成概率是:
P(y∣x,P)=∏t=1TP(yt∣x,P,y1,...,yt−1)P(y|x, P) = \prod_{t=1}^T P(y_t|x, P, y_1,...,y_{t-1})P(y∣x,P)=t=1∏TP(yt∣x,P,y1,...,yt−1)
其中yty_tyt是意图标签的第t个token,TTT是意图标签的长度。
比如意图标签是「寻找猫咖」,则y1y_1y1是「寻」,y2y_2y2是「找」,y3y_3y3是「猫」,y4y_4y4是「咖」——大模型会依次生成每个token,直到生成完整的意图标签。
3. Prompt的优化方法
为了提高Prompt的效果,常用的优化方法包括:
- Prompt Tuning:将Prompt转化为可学习的向量(而不是固定的文本),通过训练调整Prompt向量,让大模型更好地完成任务;
- Prefix Tuning:在输入前添加一个可学习的「前缀向量」,引导大模型生成正确的输出;
- Instruction Tuning:用大量的「指令-任务」数据训练大模型,让大模型学会理解各种指令(比如OpenAI的InstructGPT)。
五、项目实战:用Hugging Face实现上下文感知的意图识别
理论讲得再多,不如亲手写一遍代码。接下来,我们用Python和Hugging Face Transformers库,实现一个「上下文感知的意图识别系统」——它能处理多轮对话,识别意图并填充槽位。
1. 开发环境搭建
首先,安装需要的库:
pip install transformers torch datasets evaluate
- transformers:Hugging Face的预训练模型库;
- torch:PyTorch,用于模型训练;
- datasets:Hugging Face的数据集库;
- evaluate:Hugging Face的评估库。
2. 数据准备
我们使用SNIPS MultiWOZ数据集(多轮对话意图识别数据集),它包含多轮对话历史、当前输入、意图标签和槽位标签。
首先,加载数据集:
from datasets import load_dataset
# 加载SNIPS MultiWOZ数据集
dataset = load_dataset("snips_built_in_intents")
# 查看数据集结构
print(dataset)
# 输出:DatasetDict({train: 13084, validation: 700, test: 700})
数据集的每个样本包含以下字段:
- text:当前用户输入;
- context:历史对话(列表,每个元素是之前的用户输入和系统回复);
- intent:意图标签(比如「预订机票」);
- slots:槽位标签(字典,比如{“出发地”: “北京”, “目的地”: “上海”})。
为了适应模型输入,我们需要将「上下文+当前输入」拼接成一个文本:
def preprocess_function(examples):
# 拼接上下文和当前输入
inputs = []
for context, text in zip(examples["context"], examples["text"]):
context_str = " ".join([f"用户:{u} 系统:{s}" for u, s in context])
input_str = f"上下文:{context_str} 当前输入:{text}"
inputs.append(input_str)
# 处理槽位标签:将字典转化为序列标签(比如「北京」→「出发地」)
slot_labels = []
for slots in examples["slots"]:
# 假设text已经被分词,这里简化处理
tokens = examples["text"][examples["slots"].index(slots)].split()
labels = ["O"] * len(tokens) # O表示非槽位
for slot, value in slots.items():
if value in tokens:
idx = tokens.index(value)
labels[idx] = slot
slot_labels.append(labels)
return {
"input_str": inputs,
"intent": examples["intent"],
"slot_labels": slot_labels
}
# 预处理数据集
dataset = dataset.map(preprocess_function, batched=True)
3. 模型构建
我们使用BERT-base-uncased作为编码器,添加两个任务头:意图分类头和槽位填充头。
首先,加载BERT Tokenizer:
from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")
然后,定义数据_collator(将样本打包成批次):
import torch
from transformers import DataCollatorForTokenClassification
# 数据_collator:处理意图分类和槽位填充的批次
class IntentSlotDataCollator:
def __init__(self, tokenizer):
self.tokenizer = tokenizer
self.slot_collator = DataCollatorForTokenClassification(tokenizer)
def __call__(self, features):
# 处理意图分类:提取intent标签
intent_labels = torch.tensor([f["intent"] for f in features], dtype=torch.long)
# 处理槽位填充:用DataCollatorForTokenClassification处理slot_labels
slot_features = [{"input_ids": f["input_ids"], "attention_mask": f["attention_mask"], "labels": f["slot_labels"]} for f in features]
slot_batch = self.slot_collator(slot_features)
# 合并批次
batch = {
"input_ids": slot_batch["input_ids"],
"attention_mask": slot_batch["attention_mask"],
"intent_labels": intent_labels,
"slot_labels": slot_batch["labels"]
}
return batch
collator = IntentSlotDataCollator(tokenizer)
接下来,定义联合模型:
from transformers import BertPreTrainedModel, BertModel
from torch import nn
from transformers.modeling_outputs import TokenClassifierOutput, SequenceClassifierOutput
class JointIntentSlotModel(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.num_intent_labels = config.num_intent_labels
self.num_slot_labels = config.num_slot_labels
# BERT编码器
self.bert = BertModel(config)
# 意图分类头
self.intent_classifier = nn.Linear(config.hidden_size, config.num_intent_labels)
# 槽位填充头
self.slot_classifier = nn.Linear(config.hidden_size, config.num_slot_labels)
# 初始化权重
self.init_weights()
def forward(
self,
input_ids=None,
attention_mask=None,
token_type_ids=None,
position_ids=None,
head_mask=None,
inputs_embeds=None,
intent_labels=None,
slot_labels=None,
output_attentions=None,
output_hidden_states=None,
return_dict=None,
):
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# BERT编码
outputs = self.bert(
input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
# 意图分类:用[CLS]向量
cls_output = outputs[1] # [CLS]向量,形状:(batch_size, hidden_size)
intent_logits = self.intent_classifier(cls_output) # 形状:(batch_size, num_intent_labels)
# 槽位填充:用token向量
token_output = outputs[0] # 所有token的向量,形状:(batch_size, seq_len, hidden_size)
slot_logits = self.slot_classifier(token_output) # 形状:(batch_size, seq_len, num_slot_labels)
# 计算损失
total_loss = None
if intent_labels is not None and slot_labels is not None:
# 意图分类损失:交叉熵
intent_loss_fct = nn.CrossEntropyLoss()
intent_loss = intent_loss_fct(intent_logits.view(-1, self.num_intent_labels), intent_labels.view(-1))
# 槽位填充损失:交叉熵(忽略-100标签)
slot_loss_fct = nn.CrossEntropyLoss(ignore_index=self.config.pad_token_id)
slot_loss = slot_loss_fct(slot_logits.view(-1, self.num_slot_labels), slot_labels.view(-1))
# 总损失:加权和
total_loss = 0.5 * intent_loss + 0.5 * slot_loss
if not return_dict:
output = (intent_logits, slot_logits) + outputs[2:]
return ((total_loss,) + output) if total_loss is not None else output
return {
"loss": total_loss,
"intent_logits": intent_logits,
"slot_logits": slot_logits,
"hidden_states": outputs.hidden_states,
"attentions": outputs.attentions,
}
然后,加载预训练模型并设置参数:
from transformers import BertConfig
# 配置模型参数
config = BertConfig.from_pretrained("bert-base-uncased")
config.num_intent_labels = len(dataset["train"].features["intent"].names) # 意图标签数量
config.num_slot_labels = len(dataset["train"].features["slot_labels"].feature.names) # 槽位标签数量
config.pad_token_id = tokenizer.pad_token_id # 填充token的ID
# 初始化模型
model = JointIntentSlotModel.from_pretrained("bert-base-uncased", config=config)
4. 训练与评估
首先,定义评估指标:
- 意图分类:准确率(Accuracy);
- 槽位填充:F1-score(序列标注的常用指标)。
import evaluate
# 加载评估指标
intent_metric = evaluate.load("accuracy")
slot_metric = evaluate.load("seqeval")
# 定义计算指标的函数
def compute_metrics(p):
intent_logits, slot_logits = p.predictions
intent_labels = p.label_ids[0]
slot_labels = p.label_ids[1]
# 计算意图分类准确率
intent_preds = torch.argmax(torch.tensor(intent_logits), dim=1)
intent_accuracy = intent_metric.compute(predictions=intent_preds, references=intent_labels)["accuracy"]
# 计算槽位填充F1-score
slot_preds = torch.argmax(torch.tensor(slot_logits), dim=2)
# 将标签中的-100(填充)替换为O标签
slot_labels = [[label for label in seq if label != -100] for seq in slot_labels]
slot_preds = [[pred for pred, label in zip(seq, labels) if label != -100] for seq, labels in zip(slot_preds, slot_labels)]
# 将标签ID转化为标签名称
slot_labels_names = dataset["train"].features["slot_labels"].feature.names
slot_labels = [[slot_labels_names[label] for label in seq] for seq in slot_labels]
slot_preds = [[slot_labels_names[pred] for pred in seq] for seq in slot_preds]
slot_results = slot_metric.compute(predictions=slot_preds, references=slot_labels)
return {
"intent_accuracy": intent_accuracy,
"slot_f1": slot_results["overall_f1"],
"slot_precision": slot_results["overall_precision"],
"slot_recall": slot_results["overall_recall"],
}
然后,设置训练参数并启动训练:
from transformers import Trainer, TrainingArguments
# 训练参数
training_args = TrainingArguments(
output_dir="./joint-intent-slot-model",
evaluation_strategy="epoch",
learning_rate=2e-5,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
num_train_epochs=3,
weight_decay=0.01,
logging_steps=10,
save_strategy="epoch",
load_best_model_at_end=True,
)
# 初始化Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["validation"],
data_collator=collator,
compute_metrics=compute_metrics,
)
# 开始训练
trainer.train()
5. 推理测试
训练完成后,我们用模型进行推理测试:
def predict_intent_slot(context, text):
# 拼接上下文和当前输入
context_str = " ".join([f"用户:{u} 系统:{s}" for u, s in context])
input_str = f"上下文:{context_str} 当前输入:{text}"
# Tokenize输入
inputs = tokenizer(input_str, return_tensors="pt")
# 模型推理
outputs = model(**inputs)
# 预测意图
intent_logits = outputs["intent_logits"]
intent_pred = torch.argmax(intent_logits, dim=1).item()
intent_label = dataset["train"].features["intent"].names[intent_pred]
# 预测槽位
slot_logits = outputs["slot_logits"]
slot_pred = torch.argmax(slot_logits, dim=2).squeeze().tolist()
# 将Token ID转化为文本
tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"].squeeze().tolist())
# 过滤填充token和特殊token
slot_labels = []
for token, pred in zip(tokens, slot_pred):
if token not in [tokenizer.cls_token, tokenizer.sep_token, tokenizer.pad_token]:
slot_label = dataset["train"].features["slot_labels"].feature.names[pred]
slot_labels.append((token, slot_label))
# 提取槽位值
slots = {}
for token, label in slot_labels:
if label != "O":
slots[label] = token
return {
"intent": intent_label,
"slots": slots
}
# 测试多轮对话
context = [("我想订明天去上海的机票", "好的,请问出发地是哪里?")]
text = "北京"
result = predict_intent_slot(context, text)
print(result)
# 输出:{"intent": "预订机票", "slots": {"出发地": "北京", "目的地": "上海", "时间": "明天"}}
6. 结果分析
从测试结果可以看到,模型正确识别了「预订机票」的意图,并提取了「出发地=北京」「目的地=上海」「时间=明天」的槽位——这说明模型具备上下文感知的能力,能利用历史对话中的信息(「明天去上海的机票」)来填充当前输入的槽位。
六、实际应用场景:意图识别如何赋能AI原生应用
意图识别不是「为了技术而技术」,它的价值在于「赋能AI原生应用」。接下来,我们看几个典型的应用场景:
1. 智能开发助手:Copilot的“代码意图”理解
GitHub Copilot是AI原生开发工具的代表,它的核心能力是「理解开发者的自然语言指令,生成对应的代码」。比如:
- 开发者输入:「帮我写一个Python的快速排序函数」;
- Copilot识别出意图:「生成快速排序代码」;
- 提取槽位:「语言=Python」「算法=快速排序」;
- 生成代码:
python def quicksort(arr):...
。
Copilot的意图识别采用了大模型的Prompt Learning——将开发者的指令作为Prompt,引导大模型生成代码。比如Prompt是:「用户输入:帮我写一个Python的快速排序函数→代码:def quicksort(arr):…」。
2. AI插件系统:ChatGPT的“工具调用”意图识别
ChatGPT的插件系统允许ChatGPT调用外部工具(比如飞猪、12306、维基百科),而意图识别是「决定是否调用工具、调用哪个工具」的关键。比如:
- 用户输入:「帮我查一下明天北京到上海的机票价格」;
- ChatGPT识别出意图:「查询机票价格」;
- 提取槽位:「出发地=北京」「目的地=上海」「时间=明天」;
- 调用飞猪插件:传递槽位参数,获取机票价格并返回给用户。
ChatGPT的意图识别采用了Zero-shot Learning——通过「工具的描述」来识别意图。比如飞猪插件的描述是「用于查询和预订机票」,ChatGPT通过比较用户输入的语义与工具描述的语义,决定调用飞猪插件。
3. 多模态AI应用:MidJourney的“图像生成
更多推荐
所有评论(0)