基于ChatGLM2和OpenVINO™打造中文聊天助手

作者:英特尔 AI 软件工程师 杨亦诚

ChatGLM是由清华大学团队开发的是一个开源的、支持中英双语的类ChatGPT大语言模型,它能生成相当符合人类偏好的回答, ChatGLM2是开源中英双语对话模型 ChatGLM的第二代版本,在保留了初代模型对话流畅、部署门槛较低等众多优秀特性的基础之上,通过全面升级的基座模型,带来了更强大的性能,更长的上下文,并且该模型对学术研究完全开放,登记后亦允许免费商业使用。接下来我们分享一下如何基于ChatGLM2-6B和OpenVINO™工具套件来打造一款聊天机器人。

项目仓库地址:https://github.com/OpenVINO-dev-contest/chatglm2.openvino

注1:由于ChatGLM2-6B对在模型转换和运行过程中对内存的占用较高,推荐使用支持128Gb以上内存的的服务器终端作为测试平台。

注2:本文仅分享部署ChatGLM2-6B原始预训练模型的方法,如需获得自定义知识的能力,需要对原始模型进行Fine-tune;如需获得更好的推理性能,可以使用量化后的模型版本。

模型导出

第一步,我们需要下载ChatGLM2-6B模型,并将其导出为OpenVINO™所支持的IR格式模型进行部署,由于ChatGLM团队已经将6B版本的预训练模型发布在Hugging Face平台上,支持通过Transformer库进行推理,但不支持Optimum的部署方式(可以参考Llama2的文章),因此这里我们需要提取Transformer中的ChatGLM2的PyTorch模型对象,并实现模型文件的序列化。主要步骤可以分为:

1.获取PyTorch模型对象

通过Transformer库获取PyTorch对象,由于目前Transformer中原生的ModelForCausalLM类并不支持ChatGLM2模型架构,因此需要添加trust_remote_code=True参数,从远程模型仓库中获取模型结构信息,并下载权重。

model = AutoModel.from_pretrained(args.model_id,

                                  trust_remote_code=True).float()

2. 模拟并获取模型的输入输出参数

在调用torch.onnx.export接口将模型对象导出为ONNX文件之前,我们首先需要获取模型的输入和输出信息。由于ChatGLM2存在KV cache机制,因此这个步骤中会模拟第一次文本生成时不带cache的输入,并将其输出作为第二次迭代时的cache输入,再通过第二次迭代来验证输入数据是否完整。以下分别第一次和第二次迭代的PyTorch代码:

outputs = model.forward(**input_tensors)



outputs2 = model.forward(input_ids=input_ids,

                         attention_mask=attention_mask,

                         position_ids=position_ids,

                         past_key_values=past_key_values)

3. 导出为ONNX格式

在获取完整的模型输入输出信息后,我们可以利用torch.onnx.export接口将模型导出为ONNX文件,如果通过模型结构可视化工具查看该文件的话,不难发现原始模型对象中attention_mask这个input消失了,个人理解是因为这个attention_mask对模型的输出结果没有影响,并且其实际功能已经被position_ids 代替了,所以ONNX在转化模型的过程中自动将其优化掉了。

4. 利用OpenVINO Model Optimizer进行格式转换

最后一步可以利用OpenVINO™ 的Model Optimizer工具将模型文件转化为IR格式,并压缩为FP16精度,将较原始FP32模式,FP16模型可以在保证模型输出准确性的同时,减少磁盘占用,并优化运行时的内存开销。

模型部署

当完成IR模型导出后,我们首先需要构建一个简单的问答系统pipeline,测试效果。如下图所示,Prompt提示会送入Tokenizer进行分词和词向量编码,然后有OpenVINO™推理获得结果(蓝色部分),来到后处理部分,我们会把推理结果进行进一步的采样和解码,最后生成常规的文本信息。这里Top-K以及Top-P作为答案的筛选方法,最终从筛选后的答案中进行随机采样输出结果。

图:ChatGLM2问答任务流程

整个pipeline的大部分代码都可以套用文本生成任务的常规流程,其中比较复杂一些的是OpenVINO™推理部分的工作,由于ChatGLM2-6B文本生成任务需要完成多次递归迭代,并且每次迭代会存在cache缓存,因此我们需要为不同的迭代轮次分别准备合适的输入数据。接下来我们详细解构一下模型的运行逻辑:

图:ChatGLM2-6B模型输入输出原理

ChatGLM2的IR模型的输入主要由三部分组成:

  • input_ids是向量化后的提示输入
  • position_ids用来描述输入的位置信息,例如原始的prompt数据为“How are you”, 那这是position_ids就是[[1,2,3]], 如果输入为原始prompt的后的第一个被预测词:”I”, 那position_ids则为[[4]], 以此类推。
  • past_key_values.x是由一连串数据构成的集合,用来保存每次迭代过程中可以被共享的cache.

ChatGLM2的IR模型的输出则由两部分组成:

  • Logits为模型对于下一个词的预测,或者叫next token
  • present_key_values.x则可以被看作cache,直接作为下一次迭代的past_key_values.x值

整个pipeline在运行时会对ChatGLM2模型进行多次迭代,每次迭代会递归生成对答案中下一个词的预测,直到最终答案长度超过预设值max_sequence_length,或者预测的下一个词为终止符eos_token_id。

  • 第一次迭代

如图所示在一次迭代时(N=1)input_ids为提示语句,此时我们还需要利用Tokenizer分词器将原始文本转化为输入向量,而由于此时无法利用cache进行加速,past_key_values.x系列向量均为空值,这里我们会初始化一个维度为[0,1,2,128]的空值矩阵

  • 第N次迭代

当第一次迭代完成后,会输出对于答案中第一个词的预测Logits,以及cache数据,我们可以将这个Logits作为下一次迭代的input_ids再输入到模型中进行下一次推理(N=2), 此时我们可以利用到上次迭代中的cache数据也就是present_key_values.x,而无需每次将完整的“提示+预测词”一并送入模型,从而减少一些部分重复的计算量。这样周而复始,将当前的预测词所谓一次迭代的输入,就可以逐步生成所有的答案。

详细代码如下,这里可以看到如果past_key_values等于None就是第一次迭代,此时需要构建一个值均为空的past_key_values系列,如果不为None则会将真实的cache数据加入到输入中。

            if past_key_values is not None:

                new_position_id += 1

                inputs["position_ids"] = new_position_id

                inputs.update(past_key_values)

            else:

                inputs["position_ids"] = position_ids

                shape_input_ids = input_ids.shape

                for input_name in past_names:

                    model_inputs = self.model.input(input_name)

                    shape = model_inputs.get_partial_shape()

                    if shape[0].is_dynamic:

                        shape[0] = 0

                    if shape[1].is_dynamic:

                        shape[1] = shape_input_ids[0]

                    inputs[input_name] = Tensor(

                        model_inputs.get_element_type(), shape.get_shape())

测试输出如下:

命令:python3 generate_ov.py -m  "THUDM/chatglm2-6b" -p "请介绍一下上海?"

ChatGLM2-6B回答:

“上海是中国的一个城市,位于东部沿海地区,是中国重要的经济、文化和科技中心之一。

上海是中国的一个重要港口城市,是中国重要的进出口中心之一,也是全球著名的金融中心之一。上海是亚洲和全球经济的中心之一,拥有许多国际知名金融机构和跨国公司总部。

上海是一个拥有悠久历史和丰富文化的城市。上海是中国重要的文化城市之一,拥有许多历史文化名胜和现代文化地标。上海是中国的一个重要旅游城市,吸引了大量国内外游客前来观光旅游。“

上海是一个拥有重要经济功能的现代城市。“

聊天助手

官方示例中ChatGLM2的主要用途为对话聊天,相较于问答模型模式中一问一答的形式,对话模式则需要构建更为完整的对话,此时模型在生成答案的过程中还需要考虑到之前对话中的信息,并将其作为cache数据往返于每次迭代过程中,因此这里我们需要额外设计一个模板,用于构建每一次的输入数据,让模型能够给更充分理解哪些是历史对话,哪些是新的对话问题。

图:ChatGLM2对话任务流程

这里的text模板是由“引导词+历史记录+当前问题(提示)”三部分构成:

  • 引导词:描述当前的任务,引导模型做出合适的反馈
  • 历史记录:记录聊天的历史数据,包含每一组问题和答案
  • 当前问题:类似问答模式中的问题
def build_inputs(history: list[tuple[str, str]], query: str, system: str = ""):

    prompt = "{}\n".format(system)

    for i, (old_query, response) in enumerate(history):

        prompt += "[Round {}]\n问:{}\n答:{}\n".format(i + 1, old_query, response)

        prompt += "[Round {}]\n问:{}\n答:".format(len(history) + 1, query)

    print(prompt)

    return prompt

我们采用streamlit框架构建构建聊天机器人的web UI和后台处理逻辑,同时希望该聊天机器人可以做到实时交互,实时交互意味着我们不希望聊天机器人在生成完整的文本后再将其输出在可视化界面中,因为这个需要用户等待比较长的时间来获取结果,我们希望在用户在使用过程中可以逐步看到模型所预测的每一个词,并依次呈现。因此我们需要创建一个可以被迭代的方法generate_iterate,可以依次获取模型迭代过程中每一次的预测结果,并将其依次添加到最终答案中,并逐步呈现。

当完成任务构建后,我们可以通过streamlit run chat_robot.py命令启动聊天机器,并访问本地地址进行测试。这里选择了几个常用配置参数,方便开发者根据机器人的回答准确性进行调整:

  • 系统提示词:用于引导模型的任务方向
  • max_tokens: 生成句子的最大长度。
  • top-k: 从置信度对最高的k个答案中随机进行挑选,值越高生成答案的随机性也越高。
  • top-p: 从概率加起来为p的答案中随机进行挑选, 值越高生成答案的随机性也越高,一般情况下,top-p会在top-k之后使用。
  • Temperature: 从生成模型中抽样包含随机性, 高温意味着更多的随机性,这可以帮助模型给出更有创意的输出。如果模型开始偏离主题或给出无意义的输出,则表明温度过高。

注3:由于ChatGLM2-6B模型比较大,首次硬件加载和编译的时间会相对比较久

总结

作为当前最热门的双语大语言模型之一,ChatGLM2凭借在各大基准测试中出色的成绩,以及支持微调等特性被越来越多开发者所认可和使用。利用OpenVINO™构建ChatGLM2系列任务可以进一步提升其模型在英特尔平台上的性能,并降低部署门槛。

参考资料

1. Hugging Face Transformer:

https://huggingface.co/docs/transformers

2. ChatGLM2-6B Hugging Face:

THUDM/chatglm2-6b · Hugging Face

3. ChatGLM2-6B-TensorRT

GitHub - Tlntin/ChatGLM2-6B-TensorRT

猜你喜欢

转载自blog.csdn.net/gc5r8w07u/article/details/132669927