浅析GPT2中的autoregressive和BERT的autoencoding源码实现

news2024/11/25 16:36:38

经常使用BERT来做研究,因此对Encoder的架构较为熟悉,但是从来没有了解过GPT这样的Decoder架构,尤其对自回归的形式不知道源码是如何实现的。

为了方便对比和讨论,接来下所探讨的源码都是基于HuggingFace这个框架的。

Bert注意力机制

先看一看Bert这个Encoder架构是如何实现autoencoding的。在BertModel这个类中,可以看到其结构是由BertEmbeddings和BertEncoder两个重要的模块构成的。

class BertModel(BertPreTrainedModel):
    """

    The model can behave as an encoder (with only self-attention) as well as a decoder, in which case a layer of
    cross-attention is added between the self-attention layers, following the architecture described in [Attention is
    all you need](https://arxiv.org/abs/1706.03762) by Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit,
    Llion Jones, Aidan N. Gomez, Lukasz Kaiser and Illia Polosukhin.

    To behave as an decoder the model needs to be initialized with the `is_decoder` argument of the configuration set
    to `True`. To be used in a Seq2Seq model, the model needs to initialized with both `is_decoder` argument and
    `add_cross_attention` set to `True`; an `encoder_hidden_states` is then expected as an input to the forward pass.
    """

    def __init__(self, config, add_pooling_layer=True):
        super().__init__(config)
        self.config = config

        self.embeddings = BertEmbeddings(config)
        self.encoder = BertEncoder(config)

        self.pooler = BertPooler(config) if add_pooling_layer else None

        # Initialize weights and apply final processing
        self.post_init()

BertEmbeddings相对简单,因此编码过程是在BertEncoder中。那么接下来就是不断地层层拨开BertEncoder。

最终我们定位到了BertSelfAttention类,下面就是其定义的一些参数:

def __init__(self, config, position_embedding_type=None):
        super().__init__()
        if config.hidden_size % config.num_attention_heads != 0 and not hasattr(config, "embedding_size"):
            raise ValueError(
                f"The hidden size ({config.hidden_size}) is not a multiple of the number of attention "
                f"heads ({config.num_attention_heads})"
            )

        self.num_attention_heads = config.num_attention_heads
        self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
        self.all_head_size = self.num_attention_heads * self.attention_head_size

        self.query = nn.Linear(config.hidden_size, self.all_head_size) # all_head_size与hidden_size大小相同
        self.key = nn.Linear(config.hidden_size, self.all_head_size)
        self.value = nn.Linear(config.hidden_size, self.all_head_size)

        self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
        self.position_embedding_type = position_embedding_type or getattr(
            config, "position_embedding_type", "absolute"
        )
        if self.position_embedding_type == "relative_key" or self.position_embedding_type == "relative_key_query":
            self.max_position_embeddings = config.max_position_embeddings
            self.distance_embedding = nn.Embedding(2 * config.max_position_embeddings - 1, self.attention_head_size)

看到了我们耳熟能详的Q、K、V了,从源码来看就是利用了三个线性层,我把代码拿出来:

self.query = nn.Linear(config.hidden_size, self.all_head_size)
self.key = nn.Linear(config.hidden_size, self.all_head_size)
self.value = nn.Linear(config.hidden_size, self.all_head_size)

那么接下来就是对应项相乘就好了,在这里我就只给出Q和K的计算就好了,源码中还有很多细节,这里就不展开了,下面就是计算的代码:

# Take the dot product between "query" and "key" to get the raw attention scores.
attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
image-20230611205641538

其中query_layer和key_layer是经过transpose_for_scores这个方法计算得出的,它们的转换关系如下:

def transpose_for_scores(self, x):
        new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size) # self.num_attention_heads * self.attention_head_size = hidden_size
        x = x.view(*new_x_shape)
        return x.permute(0, 2, 1, 3) #  (bs, num_attention_heads, length, attention_head_size)
……
key_layer = self.transpose_for_scores(self.key(hidden_states))
value_layer = self.transpose_for_scores(self.value(hidden_states))
query_layer = self.transpose_for_scores(mixed_query_layer)

可以看出通过矩阵的乘法就实现了BERT的双向注意力机制,attention_scores就是注意力机制的得分。

GPT2注意力机制

直接上源码,GPT2Model类的定义如下:

class GPT2Model(GPT2PreTrainedModel):
    _keys_to_ignore_on_load_unexpected = [r"h\.\d+\.attn\.bias", r"h\.\d+\.attn\.masked_bias"]
    _keys_to_ignore_on_load_missing = [r"attn.masked_bias", r"h\.\d+\.attn\.masked_bias", r"h\.\d+\.attn\.bias"]

    def __init__(self, config):
        super().__init__(config)

        self.embed_dim = config.hidden_size

        self.wte = nn.Embedding(config.vocab_size, self.embed_dim)
        self.wpe = nn.Embedding(config.max_position_embeddings, self.embed_dim)

        self.drop = nn.Dropout(config.embd_pdrop)
        self.h = nn.ModuleList([GPT2Block(config, layer_idx=i) for i in range(config.num_hidden_layers)])
        self.ln_f = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_epsilon)

        # Model parallel
        self.model_parallel = False
        self.device_map = None
        self.gradient_checkpointing = False

        # Initialize weights and apply final processing
        self.post_init()

可以看出,其相对BERT开始,更直接一些。直接的地方在于,其将GPT2Block直接给出,而BERT需要很多层封装。接下来我们看一下GPT2Block中的GPT2Attention是怎么定义的。

GPT2Attention类中定义的部分内容如下(我只放了部分明显直接的代码):

class GPT2Attention(nn.Module):
    def __init__(self, config, is_cross_attention=False, layer_idx=None):
      # 在某些时候,我们可能希望模型中的某些参数参数不更新(从开始到结束均保持不变),但又希望参数保存下来,这是我们就会用到 register_buffer() 。
      self.register_buffer( 
            "bias",
            torch.tril(torch.ones((max_positions, max_positions), dtype=torch.bool)).view(
                1, 1, max_positions, max_positions
            ),
            persistent=False,
        ) # 生成了下三角矩阵,这个就是掩码的生成。
      # Layer-wise attention scaling, reordering, and upcasting
      self.scale_attn_by_inverse_layer_idx = config.scale_attn_by_inverse_layer_idx
      self.layer_idx = layer_idx
      self.reorder_and_upcast_attn = config.reorder_and_upcast_attn

      if self.is_cross_attention: # 这里假设不使用is_cross_attention,即is_cross_attention=False
          self.c_attn = Conv1D(2 * self.embed_dim, self.embed_dim)
          self.q_attn = Conv1D(self.embed_dim, self.embed_dim)
      else:
          self.c_attn = Conv1D(3 * self.embed_dim, self.embed_dim)
        self.c_proj = Conv1D(self.embed_dim, self.embed_dim)

        self.attn_dropout = nn.Dropout(config.attn_pdrop)
        self.resid_dropout = nn.Dropout(config.resid_pdrop)

可以明显看出,GPT的计算是使用了1维卷积来实现QKV权重的生成(但是不清楚为什么,看源码和nn.Linear效果差不多)。

当然,这里的Conv1D并不是使用了Pytorch的nn.Conv1D,而是自己重写的一个,我们来看看其是如何定义的,定义的源码如下:

class Conv1D(nn.Module):
    """
    1D-convolutional layer as defined by Radford et al. for OpenAI GPT (and also used in GPT-2).
    Basically works like a linear layer but the weights are transposed.(自己手写的原因)
    Args:
        nf (`int`): The number of output features.
        nx (`int`): The number of input features.
    """
    def __init__(self, nf, nx): # 假设是 Conv1D(3 * self.embed_dim, self.embed_dim) ,即不考虑is_cross_attention的情况
        super().__init__()
        self.nf = nf
        self.weight = nn.Parameter(torch.empty(nx, nf))
        self.bias = nn.Parameter(torch.zeros(nf))
        nn.init.normal_(self.weight, std=0.02)

    def forward(self, x):
        size_out = x.size()[:-1] + (self.nf,)
        x = torch.addmm(self.bias, x.view(-1, x.size(-1)), self.weight) # 将 (batch_size, seq_len, embed_dim) 变为(batch_size, seq_len, 3 * embed_dim)
        x = x.view(size_out)
        return x

这里使用了一个torch.addmm来实现了卷积计算,计算的方式就如下图所示:

image-20230611210707940

然后通过下面的代码实现了Q,K,V的权重获得。其中hidden_states.shape = (batch_size, seq_len, embed_dim), self.split_size=embed_dim。

query, key, value = self.c_attn(hidden_states).split(self.split_size, dim=2)

通过split方法就实现了上述在_int_()方法中的3 * self.embed_dim切分。

_attn方法给出了QKV的计算:

def _attn(self, query, key, value, attention_mask=None, head_mask=None):
    # Q, K矩阵相乘, 求每个 token 相对当前 sequence 所有 token 的注意力
    # [batch, heads, sequence_len, head_dim] * [batch, heads, head_dim, sequence_len] 变为  [batch, heads, sequence_len, sequence_len]
    attn_weights = torch.matmul(query, key.transpose(-1, -2))
		if self.scale_attn_weights:
       # 缩放计算,除以 sqrt(n_embd)
        attn_weights = attn_weights / torch.full(
            [], value.size(-1) ** 0.5, dtype=attn_weights.dtype, device=attn_weights.device
        )

    # Layer-wise attention scaling
    if self.scale_attn_by_inverse_layer_idx:
        attn_weights = attn_weights / float(self.layer_idx + 1)

    # 掩去 mask 位置的注意力
    # 解码时,每个位置的 token 只能跟自己以及之前位置的 token 计算注意力
    if not self.is_cross_attention:
        # if only "normal" attention layer implements causal mask
        query_length, key_length = query.size(-2), key.size(-2)
        causal_mask = self.bias[:, :, key_length - query_length : key_length, :key_length] # 使用的 self.register_buffer生成的掩码矩阵
        mask_value = torch.finfo(attn_weights.dtype).min # 获得attn_weights.dtype数值类型的最小值
       
      	# Need to be a tensor, otherwise we get error: `RuntimeError: expected scalar type float but found double`.Need to be on the same device, otherwise `RuntimeError: ..., x and y to be on the same device`
        mask_value =  ([], mask_value, dtype=attn_weights.dtype).to(attn_weights.device)
        # torch.where(condition,a,b)其中输入参数condition:条件限制,如果满足条件,则选择a,否则选择b作为输出。
        attn_weights = torch.where(causal_mask, attn_weights.to(attn_weights.dtype), mask_value)

    if attention_mask is not None:
        # Apply the attention mask
        attn_weights = attn_weights + attention_mask

    attn_weights = nn.functional.softmax(attn_weights, dim=-1)

    # Downcast (if necessary) back to V's dtype (if in mixed-precision) -- No-Op otherwise
    attn_weights = attn_weights.type(value.dtype)
    attn_weights = self.attn_dropout(attn_weights)

    # Mask heads if we want to
    if head_mask is not None:
        attn_weights = attn_weights * head_mask

    attn_output = torch.matmul(attn_weights, value)

    return attn_output, attn_weights

至此,我们可以看到causal_mask就是GPT模型的自回归重要的特征。attn_weights就是注意力机制的计算。

相关推荐:

past_key_values在P-TuningV2中的巧用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/662988.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于亚马逊云科技Serverless,朝夕光年和Second Dinner携手打造年度手游

经典的漫威IP,酷炫的卡牌对战,丰富的故事情节,这款移动游戏《MARVEL SNAP》一经上线就深得全球玩家喜爱。在The Game Awards 2022的年度颁奖典礼上,它更是以出色的表现,一举斩获最佳移动游戏奖项。 其研发公司Second …

【ElasticSearch】中文分词器

ES默认的analyzer(分词器),对英文单词比较友好,对中文分词效果不好。不过ES支持安装分词插件,增加新的分词器。 1、如何指定analyzer? 默认的分词器不满足需要,可以在定义索引映射的时候&#…

Python自动办公之合并多个PDF文件

本文基于使用pycharm平台,使用glob库和PyPDF2库实现 首先将需要合并的文件放于一个文件中 如下图 addpdf文件夹为需要合并的文件位置 good.py为代码块 代码如下 print(这个小代码仅仅用于合并pdf文件数量小于10的情况) print() from PyPDF2 import PdfMerger,P…

深度解析DuckDB的ScheduleEvents

深度解析DuckDB的ScheduleEvents 1.ScheduleEventData2.ScheduleEventsInternal3.SchedulePipeline 3.1 Event3.2 PipelineEventStack3.3 主逻辑4.可视化总结 书接上回熬夜三晚之深度解析DuckDB MetaPipeline,MetaPipeline在初始化的时候会构建出下面几个&#xff1…

【ARMv8/v9 异常模型入门及渐进2 - 系统控制寄存器 SCTRL_ELx 介绍】

文章目录 SCTRL_ELx 介绍背景ARMv8 SCTLR_ELx 介绍ARMv9 SCTLR_ELx 介绍 SCTRL_ELx 介绍背景 由于在做DFD 测试过程中需要测试 EL1 状态下的 self-hosted trace 功能,但是这个测试是在UEFI中做的,在开发验证阶段UEFI默认是运行在EL3 下的,所…

mac安装VsCode遇到的问题

万事开头难,头一次在安装生产工具的时候,就遇到了这么棘手的问题。百度和Google都试过了,网上的所有方式对我都没效果。最终自己阴差阳错解决了,我看内外网反馈这个问题的还挺多,在这里记录一下,希望可以帮…

Docker专题系列之十三:docker容器内安装vim编辑器

在使用docker时,有时候我们需要编辑配置文件,需要使用vim或者vi命令,但是会发现: root20ab69bedcdb:/etc/mysql# vim my.cnf bash: vim: command not found这是因为vim没有安装,使用如下命令安装: apt-ge…

Tomcat架构设计

Servlet规范 Servlet是JavaEE规范中的一种,主要是为了扩展Java作为Web服务的功能,统一定义了对应的接口,如Servlet接口,HttpRequest接口,HttpResponse接口,Filter接口。然后由具体的服务厂商来实现这些接口…

adroit java反编译

环境准备 靶机链接:百度网盘 请输入提取码 提取码:4e5y 虚拟机网络链接模式:桥接模式 攻击机系统:kali linux 2021.1 信息收集 1.探测目标靶机ip地址 arp-scan -l 2.用nmap探测靶机开放端口和服务情况 nmap -p- -A -T4 19…

二分类结局变量Logistic回归临床模型预测—— 模型评价(一)

本节讲的是二分类结局变量的临床模型预测,与之前讲的Cox回归不同,https://lijingxian19961016.blog.csdn.net/article/details/124088364https://lijingxian19961016.blog.csdn.net/article/details/124088364https://lijingxian19961016.blog.csdn.net/article/details/1240…

基于springboot+Redis的前后端分离项目(二)-【黑马点评】

🎁🎁资源文件分享 链接:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA?pwdeh11 提取码:eh11 商户查询缓存,缓存更新策略,缓存穿透 商户查询缓存a.什么是缓存1.为什么要使用缓存2.如何使用缓存 b.添加商…

一文读懂ISM频段

ISM(工业、科学、医疗)频段为国际电信联盟(ITU)《无线电规则》定义的指定无线电频段。这些频段是为电信之外的其他射频用途挪出的频段。因此, ISM频段虽然理论上可用于电信用途,但使用ISM频段的电信设备必须…

南卡OE Pro开放式耳机开售,将音质和舒适度提升至行业巅峰!

随着科技的不断发展和创新,开放式耳机作为一种全新的音频体验方式正逐渐走红。而在这个充满竞争的市场上,南卡品牌的最新款OE Pro开放式耳机如今上市,以其出色的表现和全能的功能成为行业的颠覆者。 南卡品牌在骨传导耳机音频技术领域有着超过…

【系统开发】尚硅谷 - 谷粒商城项目笔记(三):OSS阿里云存储

文章目录 OSS阿里云存储OSS基础配置OSS跨域设置原生sdk方式上传阿里云sdk方式上传将oss配置加入nacos加入网关的路由分发和前端整合测试 OSS阿里云存储 OSS基础配置 上传的账号信息存储在应用服务器 上传前先找应用服务器要一个policy上传策略,生成防伪签名 OSS跨…

帮助中心的设计指南

帮助中心是一个网站或应用程序的重要组成部分,因为它可以让用户轻松找到他们需要的信息。正确设计和实施一个高效的帮助中心可以确保用户满意度提高,并增加品牌忠诚度。本文将介绍如何设计一个优秀的帮助中心。 确定帮助中心的目标 在设计帮助中心之前&…

风控安全产品系统设计的个人感悟

背景 本篇文章会从系统架构设计的角度,分享在对业务安全风控相关基础安全产品进行系统设计时遇到的问题难点及其解决方案。 内容包括三部分:(1)风控业务架构;(2)基础安全产品的职责&#xff1…

重新安装ROG Armoury Crate

文章目录 使用官方卸载工具卸载奥创安装奥创 更新奥创之后 Aura Sync 坏了,无法调整灯光,打开 Aura Creator 提示服务运行异常: 在任务管理器中随便禁用了几个奥创、Asus的服务后,再次打开 Aura Creator,提示出了具…

【码银送书第一期】通用人工智能:初心与未来

目录 前言 正文 内容简介 作者简介 译者简介 目录 前言 自20世纪50年代图灵在其划时代论文《计算机器与智能》中提出“图灵测试”以及之后的达特茅斯研讨会开始,用机器来模仿人类学习及其他方面的智能,即实现“人工智能”(Artificial …

CSS实现盒子模型水平居中、垂直居中、水平垂直居中的多种方法

CSS实现盒子模型水平居中、垂直居中、水平垂直居中的多种方法 CSS实现盒子模型水平居中的方法 水平居中效果图 水平居中 全局样式 .parent { color: #FFFFFF; height: 200px; width: 200px; margin: 0 auto; background-color: #000000;} .child { widt…

No module named ‘torch_geometric‘解决办法

参考链接: https://blog.csdn.net/weixin_47779152/article/details/120570367 https://data.pyg.org/whl/torch-1.12.0%2Bcu113.html https://blog.csdn.net/qq_44832009/article/details/129351554 https://blog.csdn.net/qq_43750528/article/details/130644899 …