> For the complete documentation index, see [llms.txt](https://kerasnoone.gitbook.io/garnet/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kerasnoone.gitbook.io/garnet/shi-jian-ji-qiao/gong-cheng-shi-jian/shi-ji-xiang-mu/dao-pai-yi-tu-shi-bie.md).

# 倒排意图识别

## 业务难点

搭建业务FAQ系统, 任务型对话机器人系统, 必不可少的是意图识别的能力. 如果使用BERT等深度模型构建解决方案, 在项目的初期, 会面临以下的问题:

* 没有线上运行累积的数据, 训练语料难以获取
* 业务知识不足. FAQ知识, 机器人的任务能力, 都需要逐渐积累. 缺少丰富意图, 会导致此时训练得到的任务模型泛化性存在问题

在实际业务中, 遇到这种冷启动问题, 往往会采用迁移学习或模版生成语料的方式, 在初期的准确率常常存在较大问题.

## 场景特点

业务知识FAQ, 业务任务对话机器人, 用来解决用户的现实问题, 实际场景中用户的问题一般都是短问题, 绝大多数不会超过20个字, 甚至不少高频意图使用5个左右的文字即可描述清晰, 极少有长篇大论的描述, 这不仅不符合用户的输入习惯, 也可以通过多轮的设计, 将一个大的问题拆解为多步完成, 降低每一步的输入长度.

以短问题为主的业务机器人场景, 在起步阶段只要解决好短文本的意图识别率, 就能取得较好的效果.

短文本的特点是语言简练, 分词后的每个词对意图的表达都有着重要的, 不可忽略的意义. 例如`查保单`这个意图, 就是由`查询`和`保单`这两个关键词组成. 将表达`查询`的词由`查`替换为`查看`, `查找`, 或者将`保单`替换为`保险`, 问题的意图都没有发生转变.

## 初步思路

我们可以将`查询`, `保单`这种关键词称为**概念**, 将`查`, `查找`, `查询`, `查看`称为`查询`这个概念的**表达**. 而`查保单`这个**意图**是由`查询`和`保单`这两个概念组合成, 将`查询|保单`这种组合称为一个**句式**, 这个句式对应着一个意图.

按照这个思路, 对于一句用户问:

* 我们可以首先找出其中包含的所有表意概念
* 枚举这些概念的所有可能的组合, 得到一个个句式, 其中某些句式是有意义的, 各自对应着唯一的意图
* 再通过一些方法评价这些句式的匹配程度, 选择匹配程度最高的句式作为最终的输出, 也就得到了这句话的意图

这种方法关键在于:

* 由表达找到所有包含这个表达的概念
* 由概念组合找到所有包含这个组合的句式

在定义数据时, 我们说一个概念对应多种表达, 一个句式包含多个子句式, 也就是概念的组合. 而推理时要反过来, 因此我们称之为一种基于**倒排索引**的意图识别方法, 这点可以参考ES中的倒排索引理解.

## 意图推理详情

接下来会详细介绍倒排索引意图识别的流程.

### 概念抽取

要抽取一个用户问中的概念, 需要先明确表达如何获得. 我们探索了几种方式获取表达并映射到概念.

#### 分词

使用模型工具对用户问进行分词, 每个词都可以看作是一种表达, 其中重要的词就可以被用来映射到不同的概念.

一般来说, 句子中的名词, 动词, 定语成分等都有着具体的意义, 常常需要被映射为概念; 而代词, 助词, 语气成分等对意图的表达没有什么影响, 是可以被忽略掉的. 判断一个词是否重要, 我们简单的认为如果这个词可以找到任何一个概念, 就是需要继续向后探索的.

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2Feb98129a25bbe57ffc8d29df5f59c92f974ac815.png?generation=1614687611655975\&alt=media)

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2Ffd05aa48aab315f6505134bd66e591a90a52ece6.png?generation=1614687609415326\&alt=media)

这里就需要使用到**概念表达表**, 记录每个概念的所有可能的表达词汇. 前期通过人工标注得到该表, 得到哪些词表达是需要关注的. 后面累积到一定程度, 就可以训练模型自动地从新句中抽取需要关注的新词, 并结合聚类方法产生粗步标注, 再交给人工审核标注, 降低维护成本.

这里还有点需要注意, 分词方法必须支持用户词典, 主要因为:

* 随着运行积累了很多分词纠错结果, 提升分词效果
* 业务专有词汇, 通用分词模型不能很好地分开

#### 实体抽取

某些概念的表达除了词汇, 还可能是具体的实体, 例如`车牌号`这个概念除了车牌号这个词, 还可能是`沪A-XXXXX`这种形式. 或者某些词代表着一类实体概念, 如`文艺复兴有限公司`就可以能是一个`公司`的概念, 也可能是一个`地名`的概念; 又或者一串数字, 可能代表着手机号, 保险单号或者用户数字ID等概念.

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F0e9c669f88f46c5ff97f8e0c5ba4b2d964a8b3d7.png?generation=1614687609603325\&alt=media)

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F51d2ce57d13ac27747933bedc7c949ca3537ac34.png?generation=1614687610186272\&alt=media)

因此, 需要通过一些规则或NER模型, 抽取实体表达, 并将这些表达映射到对应的概念上. 一般来说一种实体表达对应一种概念, 如车牌号, 身份证号等. 但结合业务逻辑后, 一种实体可能对应多个概念, 如上面公司和地名概念的例子, 或者一个时间点, 可能对应着投保时间概念, 也可能对应着到期日时间概念.

#### 特别意向

对比这两句话: `什么时间可以办理车辆年检`, `我办理车辆年检可以去哪里`. 虽然都是问办理车辆年检的事情, 但一个是在问时间, 一个是在问地点. 两种不同的疑问意向, 也可以看作是两个不同的概念. 只是这种概念的表达不叫复杂, 不单单只是是通过单个词表达. 如`我可以在异地办理手续吗`.

这种意向包括:

* 几种常见的问题类型(6W2H):
  * Why: 为什么
  * What: 什么事/内容
  * Who: 什么人
  * When: 什么时间
  * Where: 什么地点
  * Whether: 是否
  * How/How to do: 如何
  * How much/How many: 多少
* 肯定/否定

为了尽量将感兴趣的意向概念的表达完整覆盖, 除了将一些词映射到概念, 还可以使用一些模版进行匹配. 在资源较多的情况下, 也可以使用模型进行判别.

### 概念组合

有些概念表达的意思, 可以由其他概念组合得到. 例如`新能源汽车`这个概念, 直接使用字面是一种表达方式, 但这个概念也可以理解为由`新能源`和`汽车`两个概念组合而成. `新能源`概念的表达还可以有`电动`, `混动`等, `汽车`概念的表达还可以有`轿车`, `SUV`等. 将这两个概念的表达组合得到的新表达, 也是`新能源汽车`这个更大概念的一种表达方式.

因此通过将一些常用的复杂概念拆解成简单概念的组合, 可以带来一定程度的泛化性提升. 例如在实际场景中, 经常会遇到一些固定的概念, 如`年检代办`, `车险投保`等, 但用户一般会使用这种专业的表达. 我们从业务的角度定义这种概念, 然后拆解成简单概念, 通过概念丰富的表达方式, 自动为这种复杂概念带来泛化表达.

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F3343a9faefeafbc406e46f62ac8414e863162905.png?generation=1614687616986644\&alt=media)

这里需要使用到**概念组合表**, 记录复杂概念可以由哪些简单概念组合而成. 在推理过程中, 枚举所有可能的组合方式, 将可能的组合概念添加到当前的概念列表中, 一同进入下一步的句式召回.

### 句式召回

得到完整的当前用户问对应的概念列表后, 就可以搜索所有可能的句式了.

句式作为概念的组合, 在搜索过程中, 需要枚举所有可能概念组合. 例如一个用户问经过拆解得到了包含$$n$$个概念, 所有可能的概念组合为$$\sum\_{i=1}^{n}C\_{n}^{i}$$. 每种概念组合就是一个句式, 如果这个句式在库中有对应的意图, 则这个句式就被召回了, 放入到召回句式的列表中.

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F401c26edbd0adfe5d71c394e51888a0e523d8756.png?generation=1614687610490120\&alt=media)

实际实现中枚举所有可能的概念组合复杂度太高, 需要配合剪枝降低复杂度. 首先召回每个概念的所有句式, 对不同的概念组合, 就是对其召回句式集合求交集. 组合过程中将完全匹配的句式放入的召回列表中, 直到交集为空, 实行剪枝.

由于任务意图的表达大都简洁, 所以库中的句式包含的平均概念数量也不大, 一般在第一次, 第二次概念融合时就发生了剪枝, 所以整体速度还是可行的.

### 句式排序

得到了一批召回句式后, 就需要考察这些句式的匹配程度, 进行排序, 从中选择出最优的句式, 从而确定出最合适的意图.

衡量句式的匹配程度, 可以从概念的角度出发, 认为匹配到的句式中包含的概念数量越多, 匹配程度越好. 例如`车险报价失败`这句用户问, 可以拆解提取出`车险`, `报价`, `错误`这三个概念, 并可以召回`车险|报价`, `车险|报价|错误`这两个句式, 两个句式分别对应着不同的意图和答案返回. 显然`车险|报价|错误`这个句式的意图才是用户表达的真实意图.

但使用概念匹配数量排序, 会带来两个显著的问题:

* 不同句式包含的概念数量有很大几率相等, 导致无法进一步排序区分
* 考虑到概念组合的问题, 复杂概念具有排序劣势, 召回后排序靠前的几率低, 无法利用复杂概念的泛化优势

因此在实际工程中, 采用的评价匹配程度的指标是: **句式概念覆盖字符数/用户问总字符数**. 可以参考下图.

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F958b7e3b133aec3ec7d41a1dd2067f7dfae8e351.png?generation=1614687609891994\&alt=media)

## 工程实现

### 数据结构

在数据层面, 整体是在维护一个**意图-句式-概念(复杂概念)-表达**的层级结构:

* 意图与句式是一对多的关系, 一个意图包含很多句式
* 一个句式中包含很多概念
* 一个概念有多种表达

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F33bbaddbb0d036542ba9e4a7daf7d874e6461b28.png?generation=1614687610207872\&alt=media)

可以看出这种类似于树形层级的结构维护着一级一级的知识以及临级之间的关系, 因此这种意图识别的方法也可以称为**基于知识体系的意图识别**.

工程中, 使用SQL数据库存储这些知识, 主要包含三张表:

* 概念表: 以概念为主键, 存储每个概念的所有表达. 表中同时维护简单概念和复杂概念, 每种概念的表达除了词汇, 还可以由概念组合而成
* 意图句式表: 以句式为主键, 存储句式的内容, 以及句式对应的意图ID
* 意图表: 存储管理意图ID, 及意图的业务属性

### 线上推理

建立的知识体系从根到叶, 由意图到表达, 而线上推理的过程则反过来, 先确定表达, 然后逐层聚合, 直到召回句式, 排序评分后输出最匹配句式对应的意图.

为了最小化检索时间, 采用了[倒排索引](https://zh.wikipedia.org/wiki/%E5%80%92%E6%8E%92%E7%B4%A2%E5%BC%95)这种数据结构来存储对应关系. 工程中, 我们在redis中以key value pair的形式维护了三张倒排索引表:

* 表达词-概念倒排索引
* 简单概念-复杂概念倒排索引
* 概念-句式倒排索引

这样在$$O(1)$$时间复杂度的情况下, 就能找到一个表达词所有可能被覆盖的概念. 因此检索这部分的时间消耗基本都在于redis的通信过程中.

在服务的启动的阶段, 读取SQL表中的知识数据, 转换成合适的倒排存储结构, 写入redis中. 后续意图识别请求根据键值请求即可.

### 热更新

知识内容不断更新, 新的意图, 新的句式, 新的概念和表达, 人工标注的知识会不断的积累. 除了更新到数据库中存储, 还要在线上直接生效.

因此, 服务除了提供意图识别的接口外, 还需要提供意图, 句式, 概念, 表达的增删改查接口, 对redis中倒排索引的内容进行调整.

## 系统特点

### 优点

* 在短文本为主的线上环境中能够表现出很好的准确率和召回率
* 相对于深度模型计算量少, 大部分请求在10\~30ms内就可以得到结果, 且不需要使用GPU资源
* 人工干预方便
* 适合在没有对话业务数据积累, 但有业务知识积累的情况下使用, 启动简单, 也可以取得优秀的意图识别效果
* 积累了层级知识体系, 这些知识具有很高的价值, 除了在意图识别中, 还可以在数据增强, 知识图谱等任务中使用

### 不足

* 模型简单, 泛化性完全依靠知识数据, 库中缺失的知识肯定无法正确识别
* 长文本场景提取出的概念多, 搜索组合多会影响处理效率, 概念多会导致召回过多, 精确率下降严重
* 知识的积累完全依赖于人工, 需要稳定的标注团队维护, 且随着知识的积累, 知识一致性的管理难度也逐渐提高

## 探索

* 推理速度快, 可以作为召回或粗排模型前置到精排的深度模型之前
* 概念组合探索算法进一步优化, 加强剪枝的效率
* 概念没有进一步抽象, 可以探索将概念细分成状态, 操作, 诉求等不同类型
* 句式只是概念的简单集合, 没有排列顺序以及概念角色组合的区别, 可以探索更抽象高效的句式形式
