NLU是Natural Language Understanding的简称,即自然语言理解。一直以来都与NLG(Generation)任务并称为NLP两大主流任务。一般意义上的NLU常指与理解给定句子意思相关的意图识别、实体抽取、指代关系等任务,在智能对话中应用比较广泛。
具体点来说,当用户输入一句话时,机器人一般会针对该句话(也可以把历史记录给附加上)进行全方面分析,包括:
- 情感倾向分析:简单来说,一般会包括正向、中性、负向三种类型,也可以设计更多的类别。或更复杂的细粒度情感分析,比如针对其中某个实体或属性的情感,而不是整个句子的。
- 意图识别:一般都是分类模型,大部分时候都是多分类,但是也有可能是层次分类,或多标签模型。
- 多分类:给定输入文本,输出为一个Label,但Label的总数有多个。比如类型包括询问地址、询问时间、询问价格、闲聊等等。
- 层次分类:给定输入文本,输出为层次的Label,也就是从根节点到最终细粒度类别的路径。比如询问地址/询问家庭地址、询问地址/询问公司地址等等。
- 多标签分类:给定输入文本,输出不定数量的Label,也就是说每个文本可能有多个Label,Label之间是平级关系。
- 实体和关系抽取:
- 实体抽取:提取出给定文本中的实体。实体一般指具有特定意义的实词,如人名、地名、作品、品牌等等;很多时候也是业务直接相关的词。
- 关系抽取:实体之间往往有一定的关系,比如「刘亦菲」出演「天龙八部」,其中「刘亦菲」就是人名、「天龙八部」是作品名,其中的关系就是「出演」,一般会和实体作为三元组来表示。
一般经过以上这些分析后,机器人就可以对用户的输入有一个比较清晰的理解,便于接下来据此做出响应。
另外值得一提的是,上面的过程并不一定只用在对话中,只要涉及到用户输入Query需要给出响应的场景,都需要这个NLU的过程,一般也叫Query解析。
上面提到的几个分析,如果从算法的角度看,其实就两种:
- 句子级别的分类:如情感分析、意图识别、关系抽取等。也就是给一个句子,给出一个或多个Label。
- Token级别的分类:如实体抽取、阅读理解(就是给一段文本和一个问题,然后在文本中找到问题的答案)。也就是给一个句子,给出对应实体的位置。
Token级别的分类不太好理解,我们举个例子,比如下面这句话:
刘亦菲出演天龙八部。
它在标注的时候是这样的:
刘/B-PER
亦/I-PER
菲/I-PER
出/O
演/O
天/B-WORK
龙/I-WORK
八/I-WORK
部/I-WORK
。/O
在上面这个例子中,每个Token就是每个字,每个Token会对应一个Label(当然也可以多个),Label中的B表示Begin,I表示Internal,O表示Other(也就是非实体)。模型要做的就是学习这种对应关系,当给出新的文本时,能够给出每个Token的Label预测。
可以看到也就是说,它们本质上都是分类任务,只是分类的位置或标准不一样。当然了,实际应用中会有各种不同的变化和设计,但整个思路是差不多的,我们并不需要掌握这些知识,只需要知道大概是怎么一回事就好了。
1.1 句子级别的分类
接下来,我们简单介绍一下这些分类具体是怎么做的,先说句子级别的分类。回忆上一章的Embedding,那可以算是整个DeepLearning NLP的基石,我们这部分的内容也会用到Embedding。具体过程如下:
- 将给定句子或文本表征成Embedding
- 将Embedding传入一个神经网络,计算得到不同Label的概率分布
- 将上一步的Label概率分布与真实的分布做比较,并将误差回传,修改神经网络的参数
- 得到训练好的神经网络
我们举个例子,简单起见,假设Embedding维度为32维(上一章OpenAI返回的维度比较大):
如果我们是三分类,那么最简单的W就是32×3的大小(这个W就被称为模型/参数):
得到这样的结果,因为我们想要的是概率分布,所以需要对其归一化(也就是变成0-1之间的概率值,并且加起来为1)。
根据给出的y,知道这个结果告诉我们预测的Label是0,如果真实的Label是1,那0位置的这个概率分布下次就会变小,1位置的就会变大。
实际中,W往往更加复杂,可以包含任意的数组,只要最后输出变成1×3的大小即可,比如我们弄个复杂点的:
只是参数(模型)更复杂了些,其他都是一样的。
稍微复杂点的是多标签分类和层次分类,这俩因为输出的都是多个标签,处理起来要麻烦一些,不过它们的处理方式是类似的。我们以多标签分类来说明,假设有10个标签,给定输入文本,可能是其中任意多个标签。这就意味着我们需要将10个标签的概率分布都表示出来。可以针对每个标签做个二分类,也就是说输出的大小是10×2的,每一行表示「是否是该标签」的概率分布。
这里的输出每一行有两个值,分别表示标签「是/0」和「是/1」的概率,比如第一行,0的概率为0.66,1的概率为0.34。需要注意的是归一化时,我们要指定维度,否则就变成所有的值加起来为1了,这就不对了。
上面是句子级别分类(Sequence Classification)的逻辑,我们必须再次说明,实际比上面要复杂得多,但基本思路是这样的。我们在LLM时代也并不需要自己去构建模型了,本章后面会讲到如何使用LLM的API进行各类任务。
1.2 Token级别的分类
接下来看Token级别的分类,有了刚刚的基础,这个看起来就比较容易了。它最大的特点是,Embedding是针对每个Token的。也就是说,如果给定文本长度为10,假定维度依然是32,那Embedding的大小就为:(1, 10, 32)。比刚刚的(1, 32)多了个10。换句话说,这个文本的每一个Token都是一个32维的向量。再通俗一点来说,对于模型来说,无论你是1个Token、2个Token还是100个Token,都可以统一看待–都是固定维度的一个向量表示。
下面我们假设Label共5个:B-PER,I-PER,B-WORK,I-WORK,O。
注意看,每一行表示一个Token是某个Label的概率分布(也就是每一行加起来为1),比如第一行:
表示啥呢?表示第一个Token的Label是O,那真实的Label和这个预测的之间就可能有误差,通过误差就可以更新参数,从而使得之后预测时能预测到正确的Label(也就是正确位置的概率最大)。
好了,关于NLU常见问题的基本原理我们就介绍到这里了,如果你对其中的很多细节感兴趣,那么可以关注DataWhale的NLP相关教程,从小项目开始一步一步构建自己的知识体系。
2.1 LMAS GPT API
这里我们介绍openai的GPT接口,利用GPT大模型的In-Context能力进行Zero-Shot或Few-Shot的推理。这里有四个概念需要先稍微解释一下:
- GPT:全称是Generative Pretrained Transformer,生成式预训练Transformer。大家只要知道它是一个大模型的名字即可。
- In-Context:简单来说就是一种上下文能力,也就是模型只要根据输入的文本就可以自动给出对应的结果,这种能力是大模型在学习了非常多的文本后获得的。可以看作是一种内在的理解能力。
- Zero-Shot:直接给模型文本,让它给出你要的标签或输出。
- Few-Shot:给模型一些类似的Case(输入+输出),再拼上一个新的没有输出的输入,让模型给出输出。
如果对In-Context更多细节感兴趣的,可以阅读【相关文献1】。
接下来,我们就可以用同一个接口,只要通过构造不同的输入就可以完成不同的任务。换句话说,通过使用GPT大模型的In-Context能力,我们只需要输入的时候告诉模型我们的任务就行。
我们看看具体的用法:
这个是接口,可以理解为续写,这个续写可不止能帮助我们完成一段话或一篇文章,而且可以用来做各种各样的任务,比如咱们这章要讲的分类和实体提取任务。
相比上一章的接口,它的接口参数要复杂多了,重要的参数包括:
-
model:指定的模型,就是其中一个模型,大家可以根据自己的需要,参考官方链接进行选择,一般需要综合价格和效果进行权衡。
-
prompt:提示,默认为,它是模型在训练期间看到的文档分隔符,因此如果未指定Prompt,模型将像从新文档的开始一样。简单来说,就是给模型的提示语,咱们下面有例子。
-
max_tokens:生成的最大Token数,默认为16。注意这里的Token数不一定是字数(但对中文来说几乎一致)。Prompt+生成的文本,所有的Token长度不能超过模型的上下文长度(一般是2048,新的是4096,具体可以参考上面的链接)。
-
temperature:温度,默认为1。采样温度,介于0和2之间。较高的值(如0.8)将使输出更加随机,而较低的值(如0.2)将使其更加集中和确定。通常建议调整这个参数或下面的top_p,但不能同时更改两者。
-
top_p:采样topN分布,默认为1。0.1意味着Next Token只选择前10%概率的。
-
stop:停止的Token或序列,默认为null,最多4个,如果遇到该Token或序列就停止继续生成。注意生成的结果中不包含stop。
-
presence_penalty:存在惩罚,默认为0,介于-2.0和2.0之间的数字。正值会根据新Token到目前为止是否出现在文本中来惩罚它们,从而增加模型讨论新主题的可能性。
-
frequency_penalty:频率惩罚,默认为0,介于-2.0和2.0之间的数字。正值会根据新Token到目前为止在文本中的现有频率来惩罚新Token,降低模型重复生成同一行的可能性。
更多可以参考:API Reference - OpenAI API。在大部分情况下,我们只需考虑上面这几个参数即可,甚至大部分时候只需要前两个参数,其他的用默认也行。不过熟悉上面的参数将帮助你更好地使用API。
先来个最简单的情感分类的例子,我们分别展示一下Zero-Shot和Few-Shot。
再来个实体识别的例子。
上面是官方给的一个Zero-Shot的例子,我们来造一个Few-Shot的例子,实体给设置的稍微特殊一些。
Nice,不是么。大家可以尝试如果不给这个例子,它会输出什么。
2.2 ChatGPT Style
这个是接口,可以理解为对话(也就是ChatGPT),几乎可以做任意的NLP任务。它的参数和类似,我们依然介绍主要参数:
-
model:指定的模型,就是ChatGPT,大家还是可以根据实际情况参考官方给出的列表选择合适的模型。
-
messages:会话消息,支持多轮,多轮就是多条。每一条消息为一个字典,包含「role」和「content」两个字段。如:
-
temperature:和接口含义一样。
-
top_p:和接口含义一样。
-
stop:和接口含义一样。
-
max_tokens:默认无上限,其他和接口含义一样,也受限于模型的最大上下文长度。
-
presence_penalty:和接口含义一样。
-
frequency_penalty:和接口含义一样。
更多可以参考:API Reference - OpenAI API,值得再次一提的是,接口支持多轮,而且多轮非常简单,只需要把历史会话加进去就可以了。
接下来,我们采用ChatGPT方式来做类似的任务。这个输入看起来和上面是比较类似的。
我们依次尝试上面的例子:
嗯,效果也是类似的,不过在ChatGPT这里我们可以更加精简一些:
Great! ChatGPT比前面的GPT API更加「聪明」,交互更加自然。我们可以尝试以对话的方式来让它帮忙完成任务。
实际上,关于这个领域早就有了一个成熟的技术方案:Prompt工程,大家可以进一步阅读【相关文献2】。这里给出一些常见的建议:
- 清晰,切忌复杂或歧义,如果有术语,应定义清楚。
- 具体,描述语言应尽量具体,不要抽象活模棱两可。
- 聚焦,问题避免太泛或开放。
- 简洁,避免不必要的描述。
- 相关,主要指主题相关,而且是整个对话期间,不要东一瓢西一瓤。
新手容易忽略的地方:
- 没有说明具体的输出目标。
- 在一次对话中混合多个主题。
- 让语言模型做数学题。
- 没有给出想要什么的示例样本。
- 反向提示。也就是一些反面例子。
- 要求他一次只做一件事。可以将步骤捆绑在一起一次说清,不要拆的太碎。
我们来试一下情感分类的例子:
再来做一下实体的例子:
看起来还行。我们最后试一下上面的另一个例子:
我们还用了中英文混合,它也完全没问题。
大家不妨多多尝试,也可以参考【相关文献 2-10】中的写法,总的来说,它并没有什么标准答案,最多也是一种习惯或约定。大家可以自由尝试,不用有任何负担。
3.1 文档问答
文档问答和上一章的QA有点类似,不过要稍微复杂一点。它会先用QA的方法召回一个相关的文档,然后让模型在这个文档中找出问题的答案。一般的流程还是先召回相关文档,然后做阅读理解任务。阅读理解和实体提取任务有些类似,但它预测的不是具体某个标签,而是答案的Index,即start和end的位置。
还是举个例子。假设我们的问题是:“北京奥运会举办于哪一年?”
召回的文档可能是含有北京奥运会举办的新闻,比如类似下面这样的:
第29届夏季奥林匹克运动会(Beijing 2008; Games of the XXIX Olympiad),又称2008年北京奥运会,2008年8月8日晚上8时整在中国首都北京开幕。8月24日闭幕。
标注就是「2008年」这个答案的索引。
当然,一个文档里可能有不止一个问题,比如上面的文档,还可以问:“北京奥运会啥时候开幕?”,“北京奥运会什么时候闭幕”,“北京奥运会是第几届奥运会”等问题。
根据之前的NLP方法,这里实际做起来方案会比较多,也有一定的复杂度;不过总的来说还是分类任务。现在我们有了LLM,问题就变得简单了。依然是两步:
- 召回:与上一章的QA类似,这次召回的是Doc,这一步其实就是相似Embedding选择最相似的。
- 回答:将召回来的文档和问题以Prompt的方式提交给Completion/ChatCompletion接口,直接得到答案。
我们分别用两种不同的接口各举一例,首先看看接口: