【HuggingFace Transformers】OpenAIGPTModel的核心——Block源码解析

OpenAIGPTModel的核心------Block源码解析

  • [1. Block 介绍](#1. Block 介绍)
  • [2. Block类 源码解析](#2. Block类 源码解析)
  • [3. Attention类 源码解析](#3. Attention类 源码解析)
  • [4. MLP类 源码解析](#4. MLP类 源码解析)

1. Block 介绍

GPT 模型中,BlockTransformer 架构的核心组成部分。每个 Block 主要由三个部分构成:AttentionMLP 以及两个Layer Norm 。首先,Attention 层负责计算输入中各位置之间的注意力权重,并生成加权的表示。接着,将Attention 的输出与输入进行残差连接,并通过第一个Layer Norm 层进行层归一化,形成中间状态。随后,MLP 层进一步处理这些中间状态,通过激活函数引入非线性变换。最后将MLP 层的输出和输入进行残差连接,并通过第二个Layer Norm 层进行层归一化,最终输出Block 的计算结果。这样,Block 可以有效地提取和转换序列中的复杂特征,并支持深层模型的训练和推理。Block 的结构如下:

图片地址:Improving Language Understanding by Generative Pre-Training

2. Block类 源码解析

源码地址:transformers/src/transformers/models/openai/modeling_openai.py

python 复制代码
# -*- coding: utf-8 -*-
# @time: 2024/9/3 20:42
from torch import nn
from transformers.models.openai.modeling_openai import Attention, MLP


class Block(nn.Module):
    def __init__(self, n_positions, config, scale=False):
        super().__init__()
        nx = config.n_embd
        self.attn = Attention(nx, n_positions, config, scale)  # 定义 Attention 层
        self.ln_1 = nn.LayerNorm(nx, eps=config.layer_norm_epsilon)  # 定义 LayerNorm 层1
        self.mlp = MLP(4 * nx, config)  # 定义 MLP 层
        self.ln_2 = nn.LayerNorm(nx, eps=config.layer_norm_epsilon)  # 定义 LayerNorm 层2

    def forward(self, x, attention_mask=None, head_mask=None, output_attentions=False):
        # 自注意力层计算
        attn_outputs = self.attn(
            x,
            attention_mask=attention_mask,
            head_mask=head_mask,
            output_attentions=output_attentions,
        )
        a = attn_outputs[0]  # 提取注意力机制的输出结果 a

        n = self.ln_1(x + a)  # 残差连接与第一个层层归一化
        m = self.mlp(n)  # 前馈神经网络计算
        h = self.ln_2(n + m)  # 残差连接与第二个层层归一化

        # 输出
        outputs = [h] + attn_outputs[1:]
        return outputs

3. Attention类 源码解析

源码地址:transformers/src/transformers/models/openai/modeling_openai.py

python 复制代码
# -*- coding: utf-8 -*-
# @time: 2024/9/3 20:44

import math
import torch

from torch import nn
from transformers.pytorch_utils import Conv1D, find_pruneable_heads_and_indices, prune_conv1d_layer


class Attention(nn.Module):
    def __init__(self, nx, n_positions, config, scale=False):
        super().__init__()
        # 模型的隐藏状态维度 n_state 为嵌入维度 nx
        n_state = nx  # in Attention: n_state=768 (nx=n_embd)
        # [switch nx => n_state from Block to Attention to keep identical to TF implementation]
        # 检查n_state是否可以被注意力头的数量整除,如果不能整除,则抛出异常
        if n_state % config.n_head != 0:
            raise ValueError(f"Attention n_state shape: {n_state} must be divisible by config.n_head {config.n_head}")
        # 注册一个名为bias的缓冲区变量,用于存储一个下三角矩阵,防止未来信息泄露(适用于因果自注意力)
        self.register_buffer(
            "bias",
            torch.tril(torch.ones(n_positions, n_positions)).view(1, 1, n_positions, n_positions),
            persistent=False,
        )
        self.n_head = config.n_head  # 获取注意力头的数量
        self.split_size = n_state  # 设置split_size为n_state,用于后续的维度拆分
        self.scale = scale  # 设置是否缩放注意力权重

        self.c_attn = Conv1D(n_state * 3, nx)  # 定义一个1D卷积层c_attn,用于生成查询(Q)、键(K)和值(V),输出维度是n_state的3倍
        self.c_proj = Conv1D(n_state, nx)  # 定义一个1D卷积层c_proj,用于映射最终的注意力输出
        self.attn_dropout = nn.Dropout(config.attn_pdrop)  # 定义一个dropout层,防止注意力机制的过拟合
        self.resid_dropout = nn.Dropout(config.resid_pdrop)  # 定义一个dropout层,防止残差连接的过拟合
        self.pruned_heads = set()  # 初始化一个集合,用于存储已被剪枝的注意力头

    # 定义剪枝指定注意力头的方法(辅助工具:可选)
    def prune_heads(self, heads):
        # 如果没有指定要剪枝的头,直接返回
        if len(heads) == 0:
            return
        # 根据要剪枝的头,找到可以剪枝的头和对应的索引
        heads, index = find_pruneable_heads_and_indices(
            heads, self.n_head, self.split_size // self.n_head, self.pruned_heads
        )
        index_attn = torch.cat([index, index + self.split_size, index + (2 * self.split_size)])  # 构造要剪枝的索引,用于卷积层的权重剪枝
        # Prune conv1d layers
        self.c_attn = prune_conv1d_layer(self.c_attn, index_attn, dim=1)  # 对c_attn卷积层进行剪枝
        self.c_proj = prune_conv1d_layer(self.c_proj, index, dim=0)  # 对c_proj卷积层进行剪枝
        # Update hyper params
        self.split_size = (self.split_size // self.n_head) * (self.n_head - len(heads))  # 更新split_size
        self.n_head = self.n_head - len(heads)  # 更新n_head
        self.pruned_heads = self.pruned_heads.union(heads)  # 将剪枝的头加入已剪枝集合

    # 定义计算注意力的方法
    def _attn(self, q, k, v, attention_mask=None, head_mask=None, output_attentions=False):
        w = torch.matmul(q, k)  # 计算查询(Q)和键(K)的点积,得到注意力权重矩阵
        # 根据scale值对注意力权重矩阵是否进行缩放
        if self.scale:
            w = w / math.sqrt(v.size(-1))
        # w = w * self.bias + -1e9 * (1 - self.bias)  # TF implementation method: mask_attn_weights
        # XD: self.b may be larger than w, so we need to crop it
        b = self.bias[:, :, : w.size(-2), : w.size(-1)]  # 获取与注意力权重矩阵大小一致的下三角掩码
        w = w * b + -1e4 * (1 - b)  # 使用掩码防止未来信息泄露,并通过大负值进行掩码处理

        # 如果提供了attention mask,将attention mask加到权重矩阵 w 上
        if attention_mask is not None:
            # Apply the attention mask
            w = w + attention_mask

        # 对权重矩阵 w 进行softmax归一化,并进行dropout操作
        w = nn.functional.softmax(w, dim=-1)
        w = self.attn_dropout(w)

        # Mask heads if we want to
        # 如果提供了head mask,使用head mask以屏蔽特定的头
        if head_mask is not None:
            w = w * head_mask

        outputs = [torch.matmul(w, v)]  # 注意力权重w 与 v 相乘,并将其作为输出
        # 如果要求输出注意力权重,将注意力权重也加入输出列表
        if output_attentions:
            outputs.append(w)
        return outputs

    # 定义将多头的输出合并为原来维度的方法
    def merge_heads(self, x):
        """调整x的维度顺序,然后把x的最后两个维度合并为一个维度"""
        x = x.permute(0, 2, 1, 3).contiguous()
        new_x_shape = x.size()[:-2] + (x.size(-2) * x.size(-1),)
        return x.view(*new_x_shape)  # in Tensorflow implementation: fct merge_states

    # 定义将输入拆分成多个注意力头的方法
    def split_heads(self, x, k=False):
        """把x的最后一维拆成(num_head)和(head_dim)两个维度,然后根据k的值进行维度调整"""
        new_x_shape = x.size()[:-1] + (self.n_head, x.size(-1) // self.n_head)
        x = x.view(*new_x_shape)  # in Tensorflow implementation: fct split_states
        if k:
            return x.permute(0, 2, 3, 1)
        else:
            return x.permute(0, 2, 1, 3)

    def forward(self, x, attention_mask=None, head_mask=None, output_attentions=False):
        # 通过卷积层c_attn计算query, key, value,并进行拆分,适应多头注意力机制
        x = self.c_attn(x)
        query, key, value = x.split(self.split_size, dim=2)
        query = self.split_heads(query)
        key = self.split_heads(key, k=True)
        value = self.split_heads(value)

        # 计算注意力,获取计算后的注意力输出
        attn_outputs = self._attn(query, key, value, attention_mask, head_mask, output_attentions)
        a = attn_outputs[0]

        a = self.merge_heads(a)  # 将多头注意力的输出合并
        a = self.c_proj(a)  # 通过映射层c_proj进一步处理输出
        a = self.resid_dropout(a)  # 然后再dropout操作
        
        # 将多头注意力的输出与其他注意力输出(注意力权重)一起打包成列表后输出
        outputs = [a] + attn_outputs[1:]
        return outputs  # a, (attentions)

4. MLP类 源码解析

源码地址:transformers/src/transformers/models/openai/modeling_openai.py

python 复制代码
# -*- coding: utf-8 -*-
# @time: 2024/9/3 20:45

from torch import nn
from transformers.pytorch_utils import Conv1D
from transformers.activations import silu, gelu_new

# ACT_FNS 激活函数字典
ACT_FNS = {"relu": nn.ReLU(), "silu": silu, "gelu": gelu_new, "swish": silu}


class MLP(nn.Module):
    def __init__(self, n_state, config):  # in MLP: n_state=3072 (4 * n_embd)
        super().__init__()
        nx = config.n_embd
        self.c_fc = Conv1D(n_state, nx)  # 定义卷积层 c_fc
        self.c_proj = Conv1D(nx, n_state)  # 定义卷积层 c_proj
        self.act = ACT_FNS[config.afn]  # 从 ACT_FNS 字典中根据 config.afn 指定的激活函数名称选择激活函数
        self.dropout = nn.Dropout(config.resid_pdrop)  # 定义dropout 层,防止过拟合

    def forward(self, x):
        h = self.act(self.c_fc(x))  # 将输入x从 n_state 维度转换到 nx 维度,然后使用激活函数
        h2 = self.c_proj(h)  # 将经过激活函数处理后的数据 h 从 nx 维度转换回 n_state 维度
        return self.dropout(h2)  # 对 h2 进行dropout操作后返回结果

在这个 MLP 模块中,第一层变换将输入维度从 4 * nx 压缩到 nx,第二层变换将维度从 nx 再扩展回 4 * nx。这里为什么要这么做呢?有何作用?欢迎大家在评论区讨论,给出你的答案!


补充
Conv1D 实际上实现了一个线性变换层,与传统的全连接层(nn.Linear)非常相似。这个实现简化了线性变换操作,通过 torch.addmm 执行矩阵乘法和偏置加法,避免了使用 nn.Linear 层的额外开销,同时保持了与模型的兼容性。

Conv1D 类源码解析:

源码地址:transformers/src/transformers/pytorch_utils.py

python 复制代码
# -*- coding: utf-8 -*-
# @time: 2024/9/4 11:31

import torch

from torch import nn

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):
        super().__init__()
        self.nf = nf  # 设置输出特征的数量
        self.nx = nx  # 设置输入特征的数量
        self.weight = nn.Parameter(torch.empty(nx, nf))  # 初始化权重参数,shape为 (nx, nf)
        self.bias = nn.Parameter(torch.zeros(nf))  # 初始化偏置参数,shape为 (nf,)
        nn.init.normal_(self.weight, std=0.02)  # 用均值为0,标准差为0.02的正态分布初始化权重

    def __repr__(self) -> str:
        return "Conv1D(nf={nf}, nx={nx})".format(**self.__dict__)  # 返回该层的字符串表示,包括输出特征和输入特征的数量

    def forward(self, x):
        size_out = x.size()[:-1] + (self.nf,)  # 计算输出特征的大小,这里保持x除最后一个维度外的其他维度不变,然后将最后一个维度设置为 self.nf
        x = torch.addmm(self.bias, x.view(-1, x.size(-1)), self.weight)  # 对输入 x 进行变形,将最后一个维度与权重的维度匹配,并执行矩阵乘法加上偏置
        x = x.view(size_out)  # 将x 变回size_out的维度大小
        return x
相关推荐
孙同学要努力35 分钟前
全连接神经网络案例——手写数字识别
人工智能·深度学习·神经网络
Eric.Lee202136 分钟前
yolo v5 开源项目
人工智能·yolo·目标检测·计算机视觉
其实吧32 小时前
基于Matlab的图像融合研究设计
人工智能·计算机视觉·matlab
丕羽2 小时前
【Pytorch】基本语法
人工智能·pytorch·python
ctrey_2 小时前
2024-11-1 学习人工智能的Day20 openCV(2)
人工智能·opencv·学习
SongYuLong的博客2 小时前
Air780E基于LuatOS编程开发
人工智能
Jina AI2 小时前
RAG 系统的分块难题:小型语言模型如何找到最佳断点?
人工智能·语言模型·自然语言处理
-派神-2 小时前
大语言模型(LLM)量化基础知识(一)
人工智能·语言模型·自然语言处理
johnny_hhh2 小时前
AI大模型重塑软件开发流程:定义、应用场景、优势、挑战及未来展望
人工智能
Elastic 中国社区官方博客2 小时前
释放专利力量:Patently 如何利用向量搜索和 NLP 简化协作
大数据·数据库·人工智能·elasticsearch·搜索引擎·自然语言处理