Bert Encoder和Transformer Encoder有什么不同

前言:本篇文章主要从代码实现角度研究 Bert Encoder和Transformer Encoder 有什么不同?应该可以帮助你:

  • 深入了解Bert Encoder 的结构实现
  • 深入了解Transformer Encoder的结构实现

本篇文章不涉及对注意力机制实现的代码研究。

注:本篇文章所得出的结论和其它文章略有不同,有可能是本人代码理解上存在问题,但是又没有找到更多的文章加以验证,并且代码也检查过多遍。

观点不太一致的文章:bert-pytorch版源码详细解读_bert pytorch源码-CSDN博客 这篇文章中,存在 “这个和我之前看的transformers的残差连接层差别还挺大的,所以并不完全和transformers的encoder部分结构一致。” 但是我的分析是:代码实现上不太一样,但是本质上没啥不同,只是Bert Encoder在Attention之后多了一层Linear。具体分析过程和结论可以阅读如下文章。

如有错误或问题,请在评论区回复。

1、研究目标

这里主要的观察对象是BertModel中Bert Encoder是如何构造的?从Bert Tensorflow源码,以及transformers库中源码去看。

然后再看TransformerEncoder是如何构造的?从pytorch内置的transformer模块去看。

最后再对比不同。

2、tensorflow中BertModel主要代码如下

class BertModel(object):
    def __init__(...):
        ...得到了self.embedding_output以及attention_mask
        
        # transformer_model就代表了Bert Encoder层的所有操作
        self.all_encoder_layers = transformer_model(input_tensor=self.embedding_output, attention_mask=attention_mask,...)
        
        # 这里all_encoder_layers[-1]是取最后一层encoder的输出
        self.sequence_output = self.all_encoder_layers[-1]
        
        ...pooler层,对 sequence_output中的first_token_tensor,即CLS对应的表示向量,进行dense+tanh操作
        with tf.variable_scope("pooler"):
          first_token_tensor = tf.squeeze(self.sequence_output[:, 0:1, :], axis=1)
          self.pooled_output = tf.layers.dense(
              first_token_tensor,
              config.hidden_size,
              activation=tf.tanh,
              kernel_initializer=create_initializer(config.initializer_range))
        
def transformer_model(input_tensor, attention_mask=None,...):
    ...
    for layer_idx in range(num_hidden_layers):
        # 如下(1)(2)(3)就是每一层Bert Encoder包含的结构和操作
        with tf.variable_scope("layer_%d" % layer_idx):
            # (1)attention层:主要包含两个操作,获取attention_output,对attention_output进行dense + dropout + layer_norm
            with tf.variable_scope("attention"):
                # (1.1)通过attention_layer获得 attention_output
                attention_output
                
                # (1.2)output层:attention_output需要经过dense + dropout + layer_norm操作
                with tf.variable_scope("output"):
                    attention_output = tf.layers.dense(attention_output,hidden_size,...)
                    attention_output = dropout(attention_output, hidden_dropout_prob)
                    # “attention_output + layer_input” 表示 残差连接操作
                    attention_output = layer_norm(attention_output + layer_input)
        
            # (2)intermediate中间层:对attention_output进行dense+激活(GELU)
            with tf.variable_scope("intermediate"):
              intermediate_output = tf.layers.dense(
                  attention_output,
                  intermediate_size,
                  activation=intermediate_act_fn,)
            
            # (3)output层:对intermediater_out进行dense + dropout + layer_norm
            with tf.variable_scope("output"):
              layer_output = tf.layers.dense(
                  intermediate_output,
                  hidden_size,
                  kernel_initializer=create_initializer(initializer_range))
              layer_output = dropout(layer_output, hidden_dropout_prob)
              # "layer_output + attention_output"是残差连接操作
              layer_output = layer_norm(layer_output + attention_output)
              
              all_layer_outputs.append(layer_output)

3、pytorch的transformers库中的BertModel主要代码;

  • 其中BertEncoder对应要研究的目标
class BertModel(BertPreTrainedModel):
    def __init__(self, config, add_pooling_layer=True):
        self.embeddings = BertEmbeddings(config)
        self.encoder = BertEncoder(config)
        self.pooler = BertPooler(config) if add_pooling_layer else None
        
    def forward(...):
        # 这是嵌入层操作
        embedding_output = self.embeddings(input_ids=input_ids,position_ids=position_ids,token_type_ids=token_type_ids,...)
        
        # 这是BertEncoder层的操作
        encoder_outputs = self.encoder(embedding_output,attention_mask=extended_attention_mask,...)
        
        # 这里encoder_outputs是一个对象,encoder_outputs[0]是指最后一层Encoder(BertLayer)输出
        sequence_output = encoder_outputs[0]
        # self.pooler操作是BertPooler层操作,是先取first_token_tensor(即CLS对应的表示向量),然后进行dense+tanh操作
        # 通常pooled_output用于做下游分类任务
        pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
        
class BertEncoder(nn.Module):
    def __init__(self, config):
        ...
        self.layer = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])
        ...

    def forward(...):
        for i, layer_module in enumerate(self.layer):
            
            # 元组的append做法,将每一层的hidden_states保存到all_hidden_states;
            # 第一个hidden_states是BertEncoder的输入,后面的都是每一个BertLayer的输出
            if output_hidden_states:
                all_hidden_states = all_hidden_states + (hidden_states,)
            
            ...
            # 执行BertLayer的forward方法,包含BertAttention层 + BertIntermediate中间层 + BertOutput层
            layer_outputs = layer_module(...)
            
            # 当前BertLayer的输出
            hidden_states = layer_outputs[0]
            
            # 添加到all_hidden_states元组中
            if output_hidden_states:
                all_hidden_states = all_hidden_states + (hidden_states,)


class BertLayer(nn.Module):
    def __init__(self, config):
        self.attention = BertAttention(config)
        self.intermediate = BertIntermediate(config)
        self.output = BertOutput(config)

    def forward(...):
        # (1)Attention是指BertAttention
        # BertAttention包含:BertSelfAttention + BertSelfOutput
        # BertSelfAttention包括计算Attention+Dropout
        # BertSelfOutput包含:dense+dropout+LayerNorm,LayerNorm之前会进行残差连接
        self_attention_outputs = self.attention(...)
        # self_attention_outputs是一个元组,取[0]获取当前BertLayer中的Attention层的输出
        attention_output = self_attention_outputs[0]
        
        # (2)BertIntermediate中间层包含:dense+gelu激活
        # (3)BertOutput层包含:dense+dropout+LayerNorm,LayerNorm之前会进行残差连接
        # feed_forward_chunk的操作是:BertIntermediate(attention_output) + BertOutput(intermediate_output, attention_output)
        # BertIntermediate(attention_output)是:dense+gelu激活
        # BertOutput(intermediate_output, attention_output)是:dense+dropout+LayerNorm;
        # 其中LayerNorm(intermediate_output + attention_output)中的“intermediate_output + attention_output”是残差连接操作
        layer_output = apply_chunking_to_forward(self.feed_forward_chunk, ..., attention_output)
        

4、pytorch中内置的transformer的TransformerEncoderLayer主要代码

  • torch.nn.modules.transformer.TransformerEncoderLayer
class TransformerEncoderLayer(Module):
    '''
    Args:
    d_model: the number of expected features in the input (required).
    nhead: the number of heads in the multiheadattention models (required).
    dim_feedforward: the dimension of the feedforward network model (default=2048).
    dropout: the dropout value (default=0.1).
    activation: the activation function of intermediate layer, relu or gelu (default=relu).

    Examples::
        >>> encoder_layer = nn.TransformerEncoderLayer(d_model=512, nhead=8)
        >>> src = torch.rand(10, 32, 512)
        >>> out = encoder_layer(src)
    '''
    
    def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1, activation="relu"):
    super(TransformerEncoderLayer, self).__init__()
    self.self_attn = MultiheadAttention(d_model, nhead, dropout=dropout)
    # Implementation of Feedforward model
    self.linear1 = Linear(d_model, dim_feedforward)
    self.dropout = Dropout(dropout)
    self.linear2 = Linear(dim_feedforward, d_model)

    self.norm1 = LayerNorm(d_model)
    self.norm2 = LayerNorm(d_model)
    self.dropout1 = Dropout(dropout)
    self.dropout2 = Dropout(dropout)

    self.activation = _get_activation_fn(activation)
    
    def forward(...):
        # 过程:
        # (1)MultiheadAttention操作:src2 = self.self_attn
        # (2)Dropout操作:self.dropout1(src2)
        
        # (3)残差连接:src = src + self.dropout1(src2)
        # (4)LayerNorm操作:src = self.norm1(src)
        
        # 如下是FeedForword:做两次线性变换,为了更深入的提取特征
        # (5)Linear操作:src = self.linear1(src)
        # (6)RELU激活(默认RELU)操作:self.activation(self.linear1(src))
        # (7)Dropout操作:self.dropout(self.activation(self.linear1(src)))
        # (8)Linear操作:src2 = self.linear2(...)
        # (9)Dropout操作:self.dropout2(src2)
        
        # (10)残差连接:src = src + self.dropout2(src2)
        # (11)LayerNorm操作:src = self.norm2(src)
        src2 = self.self_attn(src, src, src, attn_mask=src_mask,
                      key_padding_mask=src_key_padding_mask)[0]
        src = src + self.dropout1(src2)
        src = self.norm1(src)
        src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
        src = src + self.dropout2(src2)
        src = self.norm2(src)
        return src
    

5、区别总结

        Transformer Encoder的结构如上图所示,代码也基本和上图描述的一致,不过代码中在Multi-Head Attention和Feed Forward之后都存在一个Dropout操作。(可以认为每层网络之后都会接一个Dropout层,是作为网络模块的一部分)

可以将Transformer Encoder过程表述为:

(1)MultiheadAttention + Dropout + 残差连接 + LayerNorm

(2)FeedForword(Linear + RELU + Dropout + Linear + Dropout) + 残差连接 + LayerNorm;Transformer默认的隐含层激活函数是RELU;

可以将 Bert Encoder过程表述为:

(1)BertSelfAttention: MultiheadAttention + Dropout

(2)BertSelfOutput:Linear+ Dropout + 残差连接 + LayerNorm; 注意:这里的残差连接是作用在BertSelfAttention的输入上,不是Linear的输入。

(3)BertIntermediate:Linear + GELU激活

(4)BertOutput:Linear + Dropout + 残差连接 + LayerNorm;注意:这里的残差连接是作用在BertIntermediate的输入上,不是Linear的输入;

进一步,把(1)(2)合并,(3)(4)合并:

(1)MultiheadAttention + Dropout + Linear + Dropout + 残差连接 + LayerNorm

(2)FeedForword(Linear + GELU激活 + Linear + Dropout) + 残差连接 + LayerNorm;Bert默认的隐含层激活函数是GELU;

所以,Bert Encoder和Transformer Encoder最大的区别是,Bert Encoder在做完Attention计算后,还会用一个线性层去提取特征,然后才进行残差连接。其次,是FeedForword中的默认激活函数不同。Bert Encoder图结构如下:

Bert 为什么要这么做?或许是多一个线性层,特征提取能力更强,模型表征能力更好。

GELU和RELU:GELU是RELU的改进版,效果更好。

Reference

  • GeLU、ReLU函数学习_gelu和relu-CSDN博客

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在Linux以命令行方式(静默方式/非图形化方式)安装MATLAB(正版)

1.根据教程,下载windows版本matlab,打开图形化界面,选择linux版本的只下载不安装 2.获取安装文件夹 3.获取许可证 4.安装 (1)跳过引用文章的2.2章节 (2)本文的安装文件夹代替引用文章的解压IS…

CSS变量和@property

CSS变量 var() CSS 变量是由CSS作者定义的实体,其中包含要在整个文档中重复使用的特定值。使用自定义属性来设置变量名,并使用特定的 var() 来访问。(比如 color: var(--main-color);)。 基本用法 CSS变量定义的作用域只在定义该…

Rio: Order-Preserving and CPU-Efficient Remote Storage Access——论文泛读

EuroSys 2023 Paper 论文阅读笔记整理 问题 现代NVMe SSD和RDMA网络提供了更高的带宽和并发性,但现有的网络存储系统(例如,基于结构的NVMe),由于存储订购保证效率低下,无法充分利用这些新设备。在这些系统…

[DevOps云实践] 3大云厂商(AWS/GoogleCP/Azure)的服务对比一览

[DevOps云实践] 3大云厂商(AWS/GoogleCP/Azure)的服务对比一览 如今有很多人在使用云端或者把他们的基础架构迁移到云端。每个云服务提供商都有其优势和劣势,大部分情况下您的选择将基于您想要在云中构建什么。您公司的各个团队也很可能会倾向于为他们的特定项目使用不同的…

【论文解读】Robust Collaborative 3D Object Detection in Presence of Pose Errors

CoAlign 摘要引言方法实验结论 摘要 协同3D对象检测利用多个代理之间的信息交换,以在存在诸如遮挡之类的传感器损伤的情况下提高对象检测的准确性。然而,在实践中,由于定位不完善而导致的姿态估计误差会导致空间消息错位,并显著降…

使用html网页播放多个视频的几种方法

前言 因为项目测试需要,我需要可以快速知道自己推流的多路视频流质量,于是我想到可以使用html网页来播放视频,实现效果极其简单,方法有好几种,以下是几种记录: 注意:测试过,VLC需要使…

微服务:Feign篇

1.什么是Feign Feign是一种声明式、模板化的HTTP客户端,可用于调用HTTP API实现微服务之间的远程服务调用。它的特点是使用少量的配置定义服务客户端接口,可以实现简单和可重用的RPC调用。 先来看我们以前利用RestTemplate发起远程调用的代码&#xff…

卡密交易系统 卡密社区SUP系统源码 分销系统平台 分销商城系统开发

卡密社区SUP系统总控源码主站分销系统功能源码 跟以前的卡盟那种控制端差不多总控可以给别人开通,分销,主站,类似自己做系统商一样,自助发卡,卡密交易系统。 搭建环境Nginx1.22 mysql 5.7 php8.1 rids 7.2 安装方法…

【C++】十大排序算法之 插入排序 希尔排序

本次介绍内容参考自:十大经典排序算法(C实现) - fengMisaka - 博客园 (cnblogs.com) 排序算法是《数据结构与算法》中最基本的算法之一。 十种常见排序算法可以分为两大类: 比较类排序:通过比较来决定元素间的相对次序…

鸿蒙开发就业前景以及发展方向分析~

鸿蒙操作系统作为华为公司自主研发的操作系统,已经成为当下炙手可热的话题。作为一个全新的操作系统,鸿蒙开发为IT行业带来了巨大的就业机会。本文将围绕鸿蒙开发的就业前景以及发展方向展开讨论。 一、鸿蒙开发就业前景 随着鸿蒙操作系统的发布&#…

二叉树——从中序与后序遍历序列构造二叉树、654. 最大二叉树、617. 合并二叉树

从中序与后序遍历序列构造二叉树 给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。 示例 1: 在这里插入代码片 输入:inorder [9,3,15…

leetcode 热题 100_和为 K 的子数组

题解一: 前缀和数组哈希表:可以计算所有子数组之和暴力求解,但复杂度太高。对于子数组求和的过程,我们可以采用前缀和数组进行优化,前缀和数组中pre[index]代表nums[0]~nusm[index]之和,当我们要计算子数组…

NLP评价指标

一、分类任务常见评估: 准确度(Accuracy) 评估预测正确的比例,精确率(Precision) 评估预测正例的查准率,召回率(Recall) 评估真实正例的查全率。如果是多分类,则每个类别各自求P、R最终求平均值。 TP(True Positives…

SwiftUI 在 App 中弹出全局消息横幅(上)

功能需求 在 SwiftUI 开发的 App 界面中,有时我们需要在全局层面向用户展示一些消息: 如上图所示:我们弹出的全局消息横幅位于所有视图之上,这意味这它不会被任何东西所遮挡;而且用户可以点击该横幅关闭它。这是怎么做到的呢? 在本篇博文中,您将学到以下内容 功能需求…

mac电脑使用pyinstaller打包python脚本

pyinstaller -F template.py 出现报错"AssertionError: Executable contains code signature!" 移除签名 codesign --remove-signature /Users/f7692281/PycharmProjects/TPtestlist/transmit_v6.0.py 打包命令 pyinstaller --windowed transmit_v6.0.py pyinst…

如何使用两个 ESP32-DevKit 开发板的 SDIO 接口测试 AT 固件?

文档参考 ESP32 SDIO AT GuideSDIO 硬件接线说明 硬件准备 两个 ESP32-DevKit 开发板10 KHz 电阻长度低于 10cm 的杜邦线 管脚ESP32 SDIO HostESP32 SDIO SlaveCLK1414CMD1515DAT022DAT144DAT21212DAT31313GNDGNDGND 1-bit SD 模式(默认)&#xff1…

HTTP代理扫描的技术解析(HTTP代理扫描的技术原理和使用方法)

HTTP代理扫描的技术解析 近年来,随着互联网的快速发展,HTTP代理扫描技术也日益成熟。HTTP代理扫描是指通过扫描网络中的HTTP代理服务器,获得有效代理的IP地址和端口,进而实现网络请求的转发。通过HTTP代理扫描,用户可…

深入了解直播美颜SDK,美颜SDK是什么?

在实现直播美颜功能的背后,美颜SDK扮演了重要的角色。今天,笔者将为大家讲解美颜SDK的定义、功能以及在直播行业中的应用。 一、美颜SDK的定义 美颜SDK是一种软件开发工具包,旨在为应用开发者提供一套实现美颜功能的接口和算法。它通常包含…

探究java反射取值与方法取值性能对比

探究java反射取值与方法取值性能对比 由于我开发框架时,经常需要对象取值。常用的取值方式有: 反射取值方法调用取值 环境 同一台电脑: jdk 21.0.2 idea 2023.3.3 1. 测试代码(常用) 1.1 反射取值 public stat…

从零开始手写RPC框架(4)

这一节主要讲述网络传输模块的代码,并且几乎每一行代码都加上了我个人理解的注释,同时也讲述了其中一些以前没见过的函数,和大致的底层运行逻辑。 目录 网络传输实体类网络传输实现基于Socket实现网络传输基于Netty实现网络传输客户端服务端 …