[DL]GLM模型解读

I-背景简介

自然语言任务可以分为三类:

  • 自然语言理解(NLU):情感分类、抽取式问答、自然语言推理
  • 无条件生成:语言建模
  • 有条件生成(seq2aeq):摘要、生成式问答、机器翻译

目前预训练语言模型主要有三种类型:自回归模型(GPT等)、自编码模型(BERT等)和编码器-解码器模型(T5等)。这三类语言模型各有优缺点,但没有一种框架能够在所有的自然语言处理任务中都表现出色。

在这里插入图片描述

上述三类模型的结构就是双向注意力或者单向注意力,那么将这两种结构融合的模型需要实现的目标就是:

  1. 双向的MLM语言模型;
  2. 从左到右的单向语言模型;
  3. 接受一段文本,从左到右的生成另一段文本。

清华大学提出了一种基于自回归空白填充的通用语言模型(GLM),来解决这个挑战。GLM 通过添加二维位置编码和允许任意顺序预测空白区域,改进了空白填充预训练,在自然语言理解任务上超越了 BERT 和 T5。

II-基本结构

GLM模型的基础架构是Transformer Encoder,并做了几点修改:

  1. 重新排列了层归一化和残差连接的顺序;
  2. 使用了单一的线性层来进行输出词的预测;
  3. 用 GeLUs 替换了 ReLU 激活函数。

GLM使用Transformer Encoder同时学习单向和双向的注意力机制。基本参数:28层、hidden size=4096、64个注意力头。

III-预训练任务

针对三类下游任务,设计了三种不同的基于自回归填空的预训练目标:

  • token-level object:针对自然语言理解任务
  • document-level object:针对无条件的长文本生成
  • sentence-level object:针对输出目标是句子或段落的有条件文本生成

token-level object

给定输入文本x=[x1,…,xn],从中span多个文本片段s=[s1,…,sm],其中每个文本片段si对应于x中l个连续的词si=[si,1,…,si,l],这些s全部由一个[mask]标记替换,构成Part A,被[mask]的多个文本片段s构成Part B。

与其他模型相比,只用一个[mask]标记替换span是重要的区别。例如,XLNet对原始位置进行编码,以便能够感知缺失标记的数量,SpanBERT用相同数量的[mask]标记替换span保持长度不变。而生成任务需要生成的文本长度是未知的,因此GLM的设置更能拟合生成任务。

模型的输入为Part A+[S]+si+[S]+sj+……,Part B的顺序是被打乱的。模型训练过程中,Part A 的词可以相互看到,但不能看到 Part B 中的任何词。Part B 的词可以看到 Part A 和 Part B 中的前置词,但不能看到 Part B 中的后续词(GLM自回归的生成Part B)。

为了实现自回归生成,每个片段都用特殊的符号 [START] 和 [END] 进行填充,分别用于输入和输出。

这样,模型就自动地在一个统一的模型中学习了一个双向编码器(用于 Part A)和一个单向解码器(用于 Part B)。

在这里插入图片描述

上图分为三个部分:

  1. 输入文本的构成:输入文本x=[x1,…,xn]被分为两部分,原文本保留部分PartA,被mask的span PartB;
  2. 模型的输入和输出:输入为PartA与PartB的拼接,且与PartB每部分由[S]隔开。同时,GLM模型的位置编码为二维位置编码,Position 1中,PartA的位置编码为从1递增,PartB的位置编码为原span在PartA中的起始位置编码;Position 2中,PartA的位置编码均为0,PartB的位置编码为span内从1开始递增。
  3. 模型的注意力掩码矩阵:通过改变它的注意力掩码,从而实现encoder-decoder架构。在PartA部分为全注意力掩码,即该部分为双向注意力。在PartB部分为单向注意力,每个token只能看到之前的文本。

多任务预训练

多任务预训练设置,目标是生成更长文本,同时与token-level目标进行共同优化。GLM 在这个设置下考虑了以下两个目标:

  1. 文档级别(document-level object):采样一个单一的区域,其长度从原始长度的 50% 到100% 之间的均匀分布中采样。该目标旨在进行长文本生成。
  2. 句子级别(sentence-level object):限制遮盖的区域必须是完整的句子。多个区域(句子)被采样,覆盖原始文本的 15% 的词数。该目标旨在进行 seq2seq 任务,其预测结果通常是完整的句子或段落。

IV-微调GLM

  • 针对分类任务

    在这里插入图片描述

  • 针对生成任务

    在这里插入图片描述

V-代码解读

modeling_chatglm.py中主要包括:GLMBlock类、ChatGLMPreTrainedModel类、ChatGLMModel类。

GLMBlock 类:这是一个包含多个子模块的Transformer层,如层归一化 (LayerNorm)、自注意力 (SelfAttention) 和门控线性单元 (GLU)。

ChatGLMPreTrainedModel 类继承自 PreTrainedModel。这个类是用于处理权重初始化以及简化下载和加载预训练模型的接口。

下面对ChatGLMModel代码进行简要分析解读。

ChatGLMModel类包含GLM模型的基础模型框架,由四部分组成,依次是embedding(单词嵌入)、rotary_pos_emb(位置嵌入)、encoder(编码器)和output_layer(输出层)。

RotaryEmbedding类实现了旋转位置编码,和相对位置编码相比,RoPE 具有更好的外推性,目前是大模型相对位置编码中应用最广的方式之一。外推性是指大模型在训练时和预测时的输入长度不一致,导致模型的泛化能力下降的问题。

# 完整的 GLM 模型,包括嵌入层、编码器、输出层
class ChatGLMModel(ChatGLMPreTrainedModel):
    def __init__(self, config: ChatGLMConfig, device=None, empty_init=True):
        super().__init__(config)
        # 如果设置了`empty_init`,创建任何 PyTorch 模块时,不初始化参数
        if empty_init:
            init_method = skip_init
        else:
            init_method = default_init
        init_kwargs = {}
        if device is not None:
            init_kwargs["device"] = device
        # 单词嵌入层
        self.embedding = init_method(Embedding, config, **init_kwargs)
        # LC
        self.num_layers = config.num_layers
        # GC
        self.multi_query_group_num = config.multi_query_group_num
        # HS
        self.kv_channels = config.kv_channels

        # SL
        self.seq_length = config.seq_length
        rotary_dim = (
            config.hidden_size // config.num_attention_heads if config.kv_channels is None else config.kv_channels
        )
        # 位置嵌入(PE)
        self.rotary_pos_emb = RotaryEmbedding(rotary_dim // 2, original_impl=config.original_rope, device=device,
                                              dtype=config.torch_dtype)
        # GLM 编码器
        self.encoder = init_method(GLMTransformer, config, **init_kwargs)
        # 输出层
        self.output_layer = init_method(nn.Linear, config.hidden_size, config.padded_vocab_size, bias=False,
                                        dtype=config.torch_dtype, **init_kwargs)
        self.pre_seq_len = config.pre_seq_len
        self.prefix_projection = config.prefix_projection
        if self.pre_seq_len is not None:
            # 如果设置了前缀序列长度(PSL)
            # 关闭所有参数的自动梯度
            for param in self.parameters():
                param.requires_grad = False
            # [0, 1, ..., PSL - 1]
            self.prefix_tokens = torch.arange(self.pre_seq_len).long()
            # 初始化前缀编码层和 Dropout
            self.prefix_encoder = PrefixEncoder(config)
            self.dropout = torch.nn.Dropout(0.1)

    def get_input_embeddings(self):
        return self.embedding.word_embeddings

    def get_prompt(self, batch_size, device, dtype=torch.half):
        # prefix_tokens = [0, 1, ..., PSL - 1]
        # [PSL] => [1, PSL] => [BS, PSL]
        prefix_tokens = self.prefix_tokens.unsqueeze(0).expand(batch_size, -1).to(device)
        # [BS, PSL, KVS=NL * HS * 2GC]
        past_key_values = self.prefix_encoder(prefix_tokens).type(dtype)
        # [BS, PSL, KVS=NL * HS * 2GC] => [BS, PSL, 2NL, GC, HS]
        past_key_values = past_key_values.view(
            batch_size,
            self.pre_seq_len,
            self.num_layers * 2,
            self.multi_query_group_num,
            self.kv_channels
        )
        
        past_key_values = self.dropout(past_key_values)
        # [BS, PSL, 2NL, GC, HS] => [2NL, PSL, BS, GC, HS] => NL * [2, PSL, BS, GC, HS]
        past_key_values = past_key_values.permute([2, 1, 0, 3, 4]).split(2)
        return past_key_values

    def forward(
            self,
            input_ids,
            position_ids: Optional[torch.Tensor] = None,
            attention_mask: Optional[torch.BoolTensor] = None,
            full_attention_mask: Optional[torch.BoolTensor] = None,
            past_key_values: Optional[Tuple[Tuple[torch.Tensor, torch.Tensor], ...]] = None,
            inputs_embeds: Optional[torch.Tensor] = None,
            use_cache: Optional[bool] = None,
            output_hidden_states: Optional[bool] = None,
            return_dict: Optional[bool] = None,
    ):
        output_hidden_states = (
            output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
        )
        use_cache = use_cache if use_cache is not None else self.config.use_cache
        return_dict = return_dict if return_dict is not None else self.config.use_return_dict
        # 输入是单词 ID,的形状为 [BS, SL]
        batch_size, seq_length = input_ids.shape
        # 将单词 ID 传递给词嵌入层得到嵌入向量
        if inputs_embeds is None:
            inputs_embeds = self.embedding(input_ids)

        # 如果设置了 PSL
        if self.pre_seq_len is not None:
            # 如果没有提供 KV 缓存,初始化为前 PSL 个前缀的词嵌入
            if past_key_values is None:
                past_key_values = self.get_prompt(batch_size=batch_size, device=input_ids.device,
                                                  dtype=inputs_embeds.dtype)
            if attention_mask is not None:
                attention_mask = torch.cat([attention_mask.new_ones((batch_size, self.pre_seq_len)),
                                            attention_mask], dim=-1)

        if full_attention_mask is None:
            if (attention_mask is not None and not attention_mask.all()) or (past_key_values and seq_length != 1):
                full_attention_mask = self.get_masks(input_ids, past_key_values, padding_mask=attention_mask)

        # 计算 PE
        # 初始化位置编码层
        rotary_pos_emb = self.rotary_pos_emb(self.seq_length)
        # 如果提供了位置 ID 就是用它检索位置嵌入矩阵
        # 如果没有,就返回嵌入矩阵的前 SL 个向量
        if position_ids is not None:
            rotary_pos_emb = rotary_pos_emb[position_ids]
        else:
            rotary_pos_emb = rotary_pos_emb[None, :seq_length]
        # [BS, SL, ES] => [SL, BS, ES]
        rotary_pos_emb = rotary_pos_emb.transpose(0, 1).contiguous()

        # 将词嵌入和位置嵌入传给编码器得到编码器输出
        hidden_states, presents, all_hidden_states, all_self_attentions = self.encoder(
            inputs_embeds, full_attention_mask, rotary_pos_emb=rotary_pos_emb,
            kv_caches=past_key_values, use_cache=use_cache, output_hidden_states=output_hidden_states
        )

        # 返回 GLM 输出,每层的 KV 缓存和每层的输出
        if not return_dict:
            return tuple(v for v in [hidden_states, presents, all_hidden_states, all_self_attentions] if v is not None)

        return BaseModelOutputWithPast(
            last_hidden_state=hidden_states,
            past_key_values=presents,
            hidden_states=all_hidden_states,
            attentions=all_self_attentions,
        )

    def quantize(self, weight_bit_width: int):
        from .quantization import quantize
        quantize(self.encoder, weight_bit_width)
        return self

ChatGLMModel类的输入参数:

- input_ids:输入分词的索引;
- attention_mask:计算attention分数时,对填充部分进行屏蔽。1表示不屏蔽,0表示屏蔽;
- position_ids:每一个输入句子中,各分词的位置索引;
- full_attention_mask:全局注意力掩码,作用是指示模型在计算自注意力时应该关注哪些位置,并忽略哪些位置;
- inputs_embeds:如果input_ids没有输入,可以直接输入一个已有的文本表示;
- past_key_values:包含预先计算的key和value隐含层,大小为:batch_size, num_heads, sequence_length-1, embed_size_per_head;
- use_cache:返回key和values向量,用于decoding;
- output_hidden_states:是否输出所有层的隐藏状态;
- return_dict:是否返回一个ModelOutput而不是tuple。

ChatGLMModel使用方法示例:

from transformers import AutoTokenizer, AutoModel
model_name = '/chatglm2-6B'  # 替换为本地模型存放路径
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model = AutoModel.from_pretrained(model_name,trust_remote_code=True).cuda(DEVICE)
# 进行基本问答
response, his = model.chat(tokenizer, "你好。", history=[])

GLM论文

预训练大模型解析:GLM

ChatGLM的基座模型GLM详细解析

ChatGLM2源码解析

  • 36
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值