ChatGLM2 源码分析:`ChatGLMForConditionalGeneration.chat, .stream_chat`

337 阅读9分钟

.chat

调用分析:

In [1]: q = '你好'

In [2]: r, his = model.chat(tokenizer, q)

In [3]: r
Out[3]: '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'

In [4]: his
Out[4]: [('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。')]

In [5]: q = '你可以做什么?'

In [6]: r, his = model.chat(tokenizer, q, his)

In [7]: r
Out[7]: '我是一个大型语言模型,可以进行自然语言处理和生成。具体来说,我可以:\n\n1.  回答问题:像人类一样回答您的问题,或者提供 相关信息。\n\n2.  提供建议:根据您的问题提供一些建议,或者提供一些参考信息。\n\n3.  进行翻译:将一种语言翻译成另一种语言,或者将一种语言的文本翻译成另一种语言的文本。\n\n4.  生成文本:根据您的问题生成一些文本,比如文章、故事、新闻报道等。\n\n5.  自动文本摘要:自动概括文本的内容,并生成摘要。\n\n6.  情感分析:判断文本中情感的程度,并返回相应的情感信息。\n\n7.  智能对话:进行智能对话,与人类交流并完成任务。\n\n请注意,我是一个机器,我的回答可能不够准确,也可能会有所误导。'

In [8]: his
Out[8]:
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'),
 ('你可以做什么?',
  '我是一个大型语言模型,可以进行自然语言处理和生成。具体来说,我可以:\n\n1.  回答问题:像人类一样回答您的问题,或者提供相关信息 。\n\n2.  提供建议:根据您的问题提供一些建议,或者提供一些参考信息。\n\n3.  进行翻译:将一种语言翻译成另一种语言,或者将一种语言的文本翻译成另一种语言的文本。\n\n4.  生成文本:根据您的问题生成一些文本,比如文章、故事、新闻报道等。\n\n5.  自动文本摘要:自动概括文本的内容,并生成摘要。\n\n6.  情感分析:判断文本中情感的程度,并返回相应的情感信息。\n\n7.  智能对话:进行智能对话,与人类交流并完成任务。\n\n请注意,我是一个机器,我的回答可能不够准确,也可能会有所误导。')]

源码:

    @torch.inference_mode()
    def chat(self, tokenizer, query: str, history: List[Tuple[str, str]] = None, max_length: int = 8192, num_beams=1,
             do_sample=True, top_p=0.8, temperature=0.8, logits_processor=None, **kwargs):
        if history is None:
            history = []
        if logits_processor is None:
            logits_processor = LogitsProcessorList()
        logits_processor.append(InvalidScoreLogitsProcessor())
        # 组织模型配置项
        gen_kwargs = {"max_length": max_length, "num_beams": num_beams, "do_sample": do_sample, "top_p": top_p,
                      "temperature": temperature, "logits_processor": logits_processor, **kwargs}
        # 将历史问答和当前提问组成整个提问,然后传给分词器得到单词ID
        inputs = self.build_inputs(tokenizer, query, history=history)
        # 提问的单词 ID 输入模型得到回答的单词概率
        outputs = self.generate(**inputs, **gen_kwargs)
        # 取第一个回答,并截断回答中的提问部分
        '''
        prompt: '你好, output: tensor([[64790, 64792,   790, 30951,   517, 30910, 30939, 30996,    13,    13,
         54761, 31211, 39701,    13,    13, 55437, 31211, 36474, 54591,   243,
           162,   148,   142, 31404, 33030, 34797, 42481, 22011, 10461, 30944,
         30943, 30941, 30978, 30949, 31123, 48895, 35214, 54622, 31123, 32616,
         39905, 31901, 31639, 31155,     2]], device='cuda:0')
        tokenizer.decode(output[0]): '[Round 1]\n\n问:你好\n\n答: 你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'
        '''
        outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):]
        # 单词概率解码得到单词
        response = tokenizer.decode(outputs)
        # 裁剪空白,替换训练时间
        response = self.process_response(response)
        # 记录历史问答
        history = history + [(query, response)]
        return response, history

    def build_inputs(self, tokenizer, query: str, history: List[Tuple[str, str]] = None):
        '''
        将历史问答和当前提问组装成整个输入
        In [1]: tokenizer.build_prompt('Q3', [('Q1', 'A1'),('Q2', 'A2')])
        Out[1]: '[Round 1]\n\n问:Q1\n\n答:A1\n\n[Round 2]\n\n问:Q2\n\n答:A2\n\n[Round 3]\n\n问:Q3\n\n答:'
        '''
        prompt = tokenizer.build_prompt(query, history=history)
        '''
        整个提问传给分词器得到单词ID
        In [2]: tokenizer(['你好'], return_tensors="pt")
        Out[2]: {
           'input_ids': tensor([[64790, 64792, 36474, 54591]]), 
           'attention_mask': tensor([[1, 1, 1, 1]]), 
           'position_ids': tensor([[0, 1, 2, 3]])
        }
        '''
        inputs = tokenizer([prompt], return_tensors="pt")
        inputs = inputs.to(self.device)
        return inputs

.stream_chat

调用分析:

In [133]: q = '你好'

In [134]: it = model.stream_chat(tokenizer, q)

In [135]: for r, his in it: print(r); print(his)
你
[('你好', '你')]
你好
[('你好', '你好')]
你好👋
[('你好', '你好👋')]
...
你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题')]
你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。')]
你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。')]

In [136]: q = '你可以做什么?'

In [137]: it = model.stream_chat(tokenizer, q, his)

In [138]: for r, his in it: print(r); print(his)
我
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'), ('你可以做什么?', '我')]
我是一款
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'), ('你可以做什么?', '我是一款')]
我是一款大型
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'), ('你可以做什么?', '我是一款大型')]
...
我是一款大型语言模型,可以进行自然语言处理和生成,以及提供各种服务和咨询。我的目标是帮助人们更方便、高效地获取信息、解决问题和交流沟通
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'), ('你可以做什么?', '我是一款大型语言模型,可 以进行自然语言处理和生成,以及提供各种服务和咨询。我的目标是帮助人们更方便、高效地获取信息、解决问题和交流沟通')]
我是一款大型语言模型,可以进行自然语言处理和生成,以及提供各种服务和咨询。我的目标是帮助人们更方便、高效地获取信息、解决问题和交流沟通。
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'), ('你可以做什么?', '我是一款大型语言模型,可 以进行自然语言处理和生成,以及提供各种服务和咨询。我的目标是帮助人们更方便、高效地获取信息、解决问题和交流沟通。')]
我是一款大型语言模型,可以进行自然语言处理和生成,以及提供各种服务和咨询。我的目标是帮助人们更方便、高效地获取信息、解决问题和交流沟通。
[('你好', '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'), ('你可以做什么?', '我是一款大型语言模型,可 以进行自然语言处理和生成,以及提供各种服务和咨询。我的目标是帮助人们更方便、高效地获取信息、解决问题和交流沟通。')]

源码:

    @torch.inference_mode()
    def stream_chat(self, tokenizer, query: str, history: List[Tuple[str, str]] = None, past_key_values=None,
                    max_length: int = 8192, do_sample=True, top_p=0.8, temperature=0.8, logits_processor=None,
                    return_past_key_values=False, **kwargs):
        # 为历史和 logit 处理器设置默认值
        if history is None:
            history = []
        if logits_processor is None:
            logits_processor = LogitsProcessorList()
        logits_processor.append(InvalidScoreLogitsProcessor())
        gen_kwargs = {"max_length": max_length, "do_sample": do_sample, "top_p": top_p,
                      "temperature": temperature, "logits_processor": logits_processor, **kwargs}
        if past_key_values is None and not return_past_key_values:
            # 如果 PKV 为空,就需要使用完整的历史对话记录构建模型输入
            inputs = self.build_inputs(tokenizer, query, history=history)
        else:
            # 如果 PKV 不为空,它是历史对话记录的 KV 缓存,
            # 只需要使用当前问题构建模型输入
            inputs = self.build_stream_inputs(tokenizer, query, history=history)
        if past_key_values is not None:
            # 得到之前输入的长度
            past_length = past_key_values[0][0].shape[0]
            # 如果有PSL, 从中减去
            if self.transformer.pre_seq_len is not None:
                past_length -= self.transformer.pre_seq_len
            # 位置 ID 都后移指定长度
            inputs.position_ids += past_length
            # attention_mask 前面添加 PL 个 1
            attention_mask = inputs.attention_mask
            attention_mask = torch.cat((attention_mask.new_ones(1, past_length), attention_mask), dim=1)
            inputs['attention_mask'] = attention_mask
        for outputs in self.stream_generate(**inputs, past_key_values=past_key_values,
                                            return_past_key_values=return_past_key_values, **gen_kwargs):
            if return_past_key_values:
                outputs, past_key_values = outputs
            # 取第一个回答,并截断回答中的提问部分
            outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):]
            '''
            q: '你好'
            iter1 response: '你'
            iter2 response: '你好'
            ...
            iterN response: '你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。'
            '''
            response = tokenizer.decode(outputs)
            # 如果回答最后一个字不是终止符
            if response and response[-1] != "�":
                # 处理时间
                response = self.process_response(response)
                # 将问题和当前回答加入历史
                new_history = history + [(query, response)]
                if return_past_key_values:
                    yield response, new_history, past_key_values
                else:
                    yield response, new_history

    def build_stream_inputs(self, tokenizer, query: str, history: List[Tuple[str, str]] = None):
        # PKV 不为空的时候调用这个函数,使用当前问题构建输入
        if history:
            # 历史不为空,只使用最后一轮的提问构建输入
            # 为了和之前的问答历史衔接,需要添加换行符
            # query = '你好', prompt = "\n\n[Round x]\n\n问:你好\n\n答:"
            prompt = "\n\n[Round {}]\n\n问:{}\n\n答:".format(len(history) + 1, query)
            '''
            将 prompt 转成单词 ID,去掉开头的 ID64790、ID64792
            In [147]: tokenizer.encode('\n\n你好', add_special_tokens=False)
            Out[147]: [30910, 13, 13, 39701]
            In [149]: tokenizer.encode('\n\n你好')
            Out[149]: [64790, 64792, 30910, 13, 13, 39701]
            '''
            input_ids = tokenizer.encode(prompt, add_special_tokens=False)
            # 去掉开头的 ID30910 
            input_ids = input_ids[1:]
            '''
            为 input_ids 生成相应的 attention_mask 和 position_ids
            In [151]: tokenizer.batch_encode_plus(
                [([13,13,39701], None)], 
                return_tensors="pt", 
                add_special_tokens=False
            )
            Out[151]: {
                'input_ids': tensor([[   13,    13, 39701]]), 
                'attention_mask': tensor([[1, 1, 1]]), 
                'position_ids': tensor([[0, 1, 2]])
            }
            '''
            inputs = tokenizer.batch_encode_plus([(input_ids, None)], return_tensors="pt", add_special_tokens=False)
        else:
            # 历史为空,仅仅使用第一轮的提问构建输入
            prompt = "[Round {}]\n\n问:{}\n\n答:".format(len(history) + 1, query)
            inputs = tokenizer([prompt], return_tensors="pt")
        inputs = inputs.to(self.device)
        return inputs