手搓多模态-05 transformer编码层

发布于:2025-04-08 ⋅ 阅读:(36) ⋅ 点赞:(0)

前情回顾

前面我们已经实现一个图像嵌入顶层模型调度

class SiglipVisionTransformer(nn.Module): ##视觉模型的第二层,将模型的调用分为了图像嵌入模型和transformer编码器模型的调用
	def __init__(self, config:SiglipVisionConfig):
		super().__init__()
		self.config = config
		self.embed_dim = config.hidden_size
		self.embeddings = SiglipVisionEmbeddings(config) ## 负责将图像嵌入成向量
		self.encoder = SiglipEncoder(config) ## 负责将向量编码成注意力相关的向量
		self.post_layer_norm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps) ## 层归一化

	def forward(self, pixel_values:torch.Tensor) -> torch.Tensor:
		"""
		pixel_values: [Batch_size,Channels,Height,Width]
		"""
		## [ Batch_size,Channels,Height,Width] -> [Batch_size,Num_Patches,Embedding_size] 
		hidden_states = self.embeddings(pixel_values) ## 将图像嵌入成向量

		# [Batch_size,Num_Patches,Embedding_size] -> [Batch_size,Num_Patches,Embedding_size]
		last_hidden_state = self.encoder(hidden_states) ## 将向量编码成注意力相关的向量

		# [Batch_size,Num_Patches,Embedding_size] -> [Batch_size,Num_Patches,Embedding_size]
		last_hidden_state = self.post_layer_norm(last_hidden_state)

		return last_hidden_state

这里我们传入一个图像数据通过SiglipVisionEmbeddings 图像编码嵌入向量此时向量不是上下文相关所以我们加入一个SiglipEncoder注意力嵌入嵌入完了之后通过归一化即可返回一个图像上下相关嵌入向量有关图像嵌入部分归一化部分之前已经提及这里我们着重实现transformer注意力

编码器结构

"Attention is all you need"这篇论文,我们可以了解到,编码器的架构如上图所示,输入嵌入 + 位置编码形成了编码器的输入,在Encoder层中会有N个这样的Encoder块,每个Encoder块中先通过一个多头注意力计算,再进行残差连接和归一化,然后再通过前向传播的MLP层,再进行一次残差连接和归一化。

这里残差连接的作用是防止梯度消失,多头注意力层可以让不同的token(在图像里面是patch)相关联,然后再通过一个MLP层增加整体的参数和模型的上限。

于是我们也创建一个SiglipEncoder层:

class SiglipEncoder(nn.Module):
	def __init__(self, config:SiglipVisionConfig):
		super().__init__()
		self.config = config
		self.embed_dim = config.hidden_size
		self.num_hidden_layers = config.num_hidden_layers
		self.layers = nn.ModuleList([SiglipEncoderLayer(config) for _ in range(self.num_hidden_layers)]) ## 多层编码器

	def forward(self, input_embeddings:torch.Tensor) -> torch.Tensor:
		hidden_states = input_embeddings
		for layer in self.layers:
			hidden_states = layer(hidden_states)
		return hidden_states

一个Encoder若干SiglipEncoderLayer具体多少个作为超参数配置修改接着我们需要实现每个SiglipEncoderLayer

SiglipEncoderLayer结构

注意:这里我们稍作修改我们在模型第二层调用这里加了一个post_layer_norm

self.post_layer_norm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps) ## 层归一化

这是因为我们希望嵌入向量进入编码之前之后一次归一化所以每个EncodeLayer我们归一化自注意力归一化MLP然后整个Encoder调用输出我们会用post_layer_norm做一次归一化参考SiglipVisionTransformer

根据之前结构我们编写如下代码

class SiglipEncoderLayer(nn.Module):
	def __init__(self, config:SiglipVisionConfig):
		super().__init__()
		self.config = config
		self.embed_dim = config.hidden_size
		self.self_atten = SiglipAttention(config) ## 注意力层
		self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps) ## 层归一化
		self.mlp = SiglipMLP(config) ## MLP层
		self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps) ## 层归一化
	
	def forward(self, hidden_states:torch.Tensor) -> torch.Tensor:
		"""
		hidden_states: [Batch_size,Num_Patches,Embedding_size]
		"""

		residual = hidden_states 
		hidden_states = self.layer_norm1(hidden_states) ## 层归一化 
		hidden_states = self.self_atten(hidden_states) ## 注意力层
		## 残差连接 [Batch_size,Num_Patches,Embedding_size]
		residual = hidden_states = hidden_states + residual	
		hidden_states = self.layer_norm2(hidden_states)
		hidden_states = self.mlp(hidden_states) ## MLP层

		## 残差连接 [Batch_size,Num_Patches,Embedding_size]
		return hidden_states + residual

MLP层的结构

我们实现简单MLP这里将自注意力的输出进行线性变换主要为了增加参数扩展模型性能上限代码如下

class SiglipMLP(nn.Module):
	
	def __init__(self, config:SiglipVisionConfig):
		super().__init__()
		self.config = config
		self.embed_dim = config.hidden_size
		self.intermediate_size = config.intermediate_size
		self.fc1 = nn.Linear(self.embed_dim, self.intermediate_size)
		self.fc2 = nn.Linear(self.intermediate_size, self.embed_dim)
		
	def forward(self, hidden_states:torch.Tensor) -> torch.Tensor:
		"""
		hidden_states: [Batch_size,Num_Patches,Embedding_size]
		"""
		hidden_states = self.fc1(hidden_states) ## [Batch_size,Num_Patches,Embedding_size] -> [Batch_size,Num_Patches,Intermediate_size]

		hidden_states = nn.functional.gelu(hidden_states,approximate="tanh") ## [Batch_size,Num_Patches,Intermediate_size] gelu激活函数

		hidden_states = self.fc2(hidden_states) ## [Batch_size,Num_Patches,Intermediate_size] -> [Batch_size,Num_Patches,Embedding_size]

		return hidden_states

值得一提这里激活函数用的gelu激活函数那我们gelu激活函数做一个简单介绍

Gelu激活函数

gelu激活函数relu激活函数变体我们一下激活函数发展

激活函数是什么?

激活函数的主要作用是提供网络的非线性建模能力。如果没有激活函数,那么该网络仅能够表达线性映射,此时即便有再多的隐藏层,其整个网络跟单层神经网络也是等价的。因此也可以认为,只有加入了激活函数之后,深度神经网络才具备了分层的非线性映射学习能力。

所以激活函数作用主要为模型引入非线性

早期的激活函数sigmoid激活函数公式和图像如下

这里图像就可以看出来这是一个非线性函数并且单调R数值放缩01之间

但是sigmoid函数有一些问题

  • 输入小于-5或者大于5时候梯度非常平缓容易导致梯度消失问题
  • 函数计算公式复杂这在梯度时候花费很大计算资源

于是为了改进这些缺点提出relu激活函数

relu激活函数公式如下

  • relu全称Rectified Linear Units ,即整流线性单元这里我们可以看到梯度保留下来计算复杂降低

但是不够完美因为小于0部分置为0模型无法小于0神经元学习任何知识所以人们进行优化提出gelu函数

Gelu函数

GELU函数全称Gaussian Error Linear Unit也叫高斯误差线性激活单元公式如下

其中φ(x)表示标准正态分布累计概率密度函数累计概率密度函数定义我们可以知道R01递增GELU函数图像所示

可以看到函数小于0部分并非简单输出0而是对一些信息做了保留同时函数更加平滑相比RELU来说GELU函数连续可导

计算复杂GELU虽然RELU计算复杂度高上不少但是人们近似公式计算GELU使得GELU函数计算复杂sigmoid函数相似这是可以接受因为改进sigmoid梯度消失问题近似公式如下