引言

 针对新手小白,一开始入门大模型面对的很多困难,许多晦涩的概念一时半会也理解不了,同时网上的资料五花八门,一方面这是我第一次写,想帮助像我一样迷茫的小白,另一方面也想借此梳理学到的东西,仅仅作为入门的文章,如有不足,还望海涵。

gpt模型架构

整体流程:

文本信息,比如“猫吃鱼的”这句语料,先经过torch.nn.embedding生成每一个词的tokens id,比如随机产生[0.234,0,345,0.456,0.567],输入到12层解码器层后,最后给出文本预测分数和基于最后一个token信息给出来的分类信息(用于下游的大模型微调任务)。

具体每一层的解码器又会依次经历:多头自注意力机制、归一化、前向传播、归一化这4个过程。后面将通过简单代码演示每个过程的基本步骤。

可以看到gpt模型架构相比于transformer的deconder部分,(1)删掉了原部分的多头注意力机制,只保留了多头自注意力力机制(2)对于最后输出层有两部分,区别于原先transformer架构只输出预测的tokes,GPT架构输出了文本预测和对应分类信息。

代码实现(分部分解析),完整的代码在文章最后:

初始化

import torch
import torch.nn as nn
import torch.nn.functional as F

# -------------------------- 超参数设置 --------------------------
vocab_size = 1000    # 词表大小
dim = 128            # 模型维度
n_heads = 4          # 注意力头数
n_layers = 3         # Decoder层数
seq_len = 64         # 最大上下文长度
head_dim = dim // n_heads
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

首先先定义词表大小、模型维度、注意力头数、解码器层数、最大上下文长度,以及每个注意力头部的维度大小(head_dim:128//4=32,决定后续q,k,v的矩阵长度是32)。

旋转位置信息

class RoPE(nn.Module):
    def __init__(self, dim):
        super(RoPE, self).__init__()
        self.dim = dim
        theta = 10000**(-torch.arange(0, dim, 2) / dim)#tensor([1.0000, 0.0100])
        self.register_buffer('theta', theta)#是 PyTorch 中用于将张量注册为模型的“缓冲区”(Buffer)
    def forward(self, x:torch.Tensor):
        #x=[B,H,L,D]
        B,H,L,D=x.shape
        pos=torch.arange(L,device=x.device)
        freqs=pos[:,None]*self.theta[None,:]
        cos = torch.cos(freqs)[None, None,:,:]
        sin = torch.sin(freqs)[None, None,:,:]
        x1, x2 = x.chunk(2, dim=-1)
        x_rot = torch.cat([x1*cos - x2*sin, x1*sin + x2*cos], dim = -1)
        return x_rot

GPT主流旋转位置信息的处理过程:区别于传统的transformer架构里面的sin,cos,GPT架构采用旋转位置信息编码的方式,目的是为了解决传统位置编码在遇到长文本(比如1248个字)的时候,计算难度高以及信息偏差大的问题。具体过程如下:根据token的维度深度,划分旋转的次数,比如dim=4,torch.arange(0,4,2)=tensor[0,2];theta=tensor[1.0000,0.0100],以下表格说明了频率值对捕捉文本距离的关系,数值越大,距离越近;反之越小,距离越远,也侧面反映出文本之间关系的远近完美契合三角函数的规律。即是x1' = x1 * cos θ - x2 * sin θ
x2' = x1 * sin θ + x2 * cos θ

维度组 i 频率值 物理含义
0 1.0000 高频:变化快,捕捉短距离关系
1 0.1000 中频
2 0.0100 低频
3 0.0010

极低频:变化慢,捕捉长距离关系

带掩码的多头子注意力机制:

class CausalSelfAttention(nn.Module):
    def __init__(self, dim,n_heads):
        super(CausalSelfAttention, self).__init__()
        self.dim = dim
        self.n_heads = n_heads
        self.head_dim = dim // n_heads
        self.rope=RoPE(self.head_dim)
        self.qkv_proj=nn.Linear(dim,dim*3)
        self.out_proj = nn.Linear(dim,dim)
        self.register_buffer("mask", torch.tril(torch.ones(seq_len, seq_len)) == 0)
    def forward(self, x:torch.Tensor):
        # x: [B, L, D]
        B, L, D = x.shape
        qkv=self.qkv_proj(x).split(D,dim=-1)
        q, k, v = [t.view(B, L, self.n_heads, self.head_dim).transpose(1, 2) for t in qkv]
        q=self.rope(q)
        k=self.rope(k)
        attn_score = q @ k.transpose(-2, -1) / (self.head_dim ** 0.5)
        attn_score.masked_fill_(self.mask[:L, :L], -1e9)
        attn_weight = F.softmax(attn_score, dim=-1)  # [B, H, L, L]
        out = attn_weight @ v  # [B, H, L, head_dim]
        out = out.transpose(1, 2).contiguous().view(B, L, D)  # 恢复为 [B, L, D]
        return self.out_proj(out)  # [B, L, D]

带掩码的多头子注意力机制是gpt模型的重中之重:

首先依据维度和注意力头数数量划分数量为head_dim的单头注意力维度(打个比方,假设评价一个人可以从128个角度评价,然后找来了4个人来评价,每一个人可以从32个角度对这个人评价,每一个来评价的人都必须准备32个问题,也就是q,然后找标签k,看哪一个和他匹配,找到k之后才会真正看到每个标签所对应的内容v,这也是qkv矩阵的机制)。为了发挥出计算机并行的效率,一下子利用self.qkv_proj=nn.Linear(dim,dim*3)创造出q,k,v来,也可以一个个创建,但是一下子创建出来是最简单方式。最后分别拿q去找对应标签k,再拿到标签具体的内容v的时候,是3*dim维度的,再统一映射为dim,也就是:self.out_proj = nn.Linear(dim,dim)。

生成掩码:mask,具体如下:

seq_len=4

python

# torch.tril() 保留下三角
[[1, 0, 0, 0],
 [1, 1, 0, 0],
 [1, 1, 1, 0],
 [1, 1, 1, 1]]

# == 0 后变成布尔掩码
[[False,  True,  True,  True],
 [False, False,  True,  True],
 [False, False, False,  True],
 [False, False, False, False]]

True 表示该位置被掩蔽,在计算注意力时会被设为 -1e9(接近负无穷),Softmax 后权重趋于 0。

def forward(self, x):
    # x: [B, L, D]
    B, L, D = x.shape
    
    # --- 步骤1:生成 Q, K, V ---
    qkv = self.qkv_proj(x).split(D, dim=-1)
    # qkv 是 tuple of 3 个 [B, L, D]
    
    # --- 步骤2:重塑为多头格式 ---
    q, k, v = [t.view(B, L, self.n_heads, self.head_dim).transpose(1, 2) for t in qkv]
    # 每个变成 [B, n_heads, L, head_dim]

我画一下维度变化:

text

x: [B, L, D]                           D = n_heads × head_dim
    ↓
Linear(dim, dim×3): [B, L, D×3]
    ↓ split(dim=-1)
q, k, v: 各 [B, L, D]
    ↓ view(B, L, n_heads, head_dim)
各 [B, L, n_heads, head_dim]
    ↓ transpose(1, 2)
各 [B, n_heads, L, head_dim]  ← 这就是多头格式!

为什么要把 n_heads 提到第2维?

因为要让每个头独立计算注意力

text

原始布局: [Batch, 序列长度, 所有头拼接]
目标布局: [Batch, 头索引, 序列长度, 每个头的维度]

这样才能用矩阵乘法一次计算所有头的注意力分数。

q = self.rope(q)  # [B, n_heads, L, head_dim]
k = self.rope(k)  # [B, n_heads, L, head_dim]

注意:RoPE 只作用于 Q 和 K,不对 V 做旋转。因为位置信息只在计算注意力分数时需要,V 本身不需要位置信息。

计算注意力分数

attn_score = q @ k.transpose(-2, -1) / (self.head_dim ** 0.5)
  • q @ k.transpose(-2, -1):形状 [B, H, L, L]

  • 除以 √head_dim 是缩放点积注意力,防止分数过大导致 Softmax 梯度消失

应用因果掩码

attn_score.masked_fill_(self.mask[:L, :L], -1e9)

只取 mask 的前 L 行 L 列。

python

# 以 L=4 为例,mask[:4, :4] 是:
[[False,  True,  True,  True],
 [False, False,  True,  True],
 [False, False, False,  True],
 [False, False, False, False]]

# 将 True 位置设为 -1e9
# 这样位置 i 只能看到 0~i,看不到未来

Softmax 和输出

attn_weight = F.softmax(attn_score, dim=-1)  # [B, H, L, L]
out = attn_weight @ v  # [B, H, L, head_dim]
out = out.transpose(1, 2).contiguous().view(B, L, D)  # 恢复为 [B, L, D]
return self.out_proj(out)  # [B, L, D]

单层解码器实现

class DecoderLayer(nn.Module):
    def __init__(self, dim, heads):
        super(DecoderLayer, self).__init__()
        self.attn=CausalSelfAttention(dim,n_heads)
        self.ln1 = nn.LayerNorm(dim)
        self.ln2 = nn.LayerNorm(dim)
        self.ffn=nn.Sequential(
            nn.Linear(dim, dim*4),
            nn.GELU(),
            nn.Linear(dim*4, dim),
        )

    def forward(self, x):
        # Pre-Norm 架构(GPT标准)
        x = x + self.attn(self.ln1(x))  # 残差连接
        x = x + self.ffn(self.ln2(x))  # 残差连接
        return x
首先计算注意力机制attn,然后制定两层归一化ln1、ln2,搭建简单的前向网络结构ffn;在forword函数里面:(1)先ln1归一化,送到多头自注意力机制,与原输入结合(2)先ln2归一化,送到前向传播网络,与原输入结合

GPT模型架构

class GPT(nn.Module):
    def __init__(self):
        super(GPT, self).__init__()
        # 词嵌入:将 token ID 转为向量
        self.embedding = nn.Embedding(vocab_size, dim)
        #堆叠 3 层 Decoder
        self.decoder_blocks=nn.Sequential(
            *[DecoderLayer(dim,n_heads)
              for _ in range(n_heads)
              ]
        )
        # 最终归一化
        self.ln_final = nn.LayerNorm(dim)

        # 输出头:从向量映射回词表
        self.head = nn.Linear(dim, vocab_size)

    def forward(self, tokens):
        x = self.embedding(tokens)  # [B, L] → [B, L, D]
        x = self.decoder_blocks(x)  # [B, L, D] → [B, L, D]
        x = self.ln_final(x)
        logits = self.head(x)  # [B, L, vocab_size]
        return logits
    @torch.no_grad()
    def generate(self, prompt_tokens, max_new_tokens=20, temperature=0.7):
        self.eval()
        cur_tokens=prompt_tokens.to(device)
        for _ in range(max_new_tokens):
            if cur_tokens.shape[1]>seq_len:
                cur_tokens = cur_tokens[:, -seq_len:]
            logits = self(cur_tokens)
            next_logit = logits[:, -1, :] / temperature
            probs = F.softmax(next_logit, dim=-1)
            next_token = torch.multinomial(probs, num_samples=1)
            cur_tokens = torch.cat([cur_tokens, next_token], dim=-1)
        return cur_tokens

首先初始化和gpt架构一致,forward返回的是

[B, L, vocab_size]
  ↑  ↑     ↑
  |  |     └── 词表大小(每个位置预测所有词的概率)
  |  └──────── 序列长度(一句话有多少个词)
  └─────────── 批次大小(一次处理几句话)

比如“猫吃鱼的”,[‘‘猫’’,‘‘吃’,‘‘鱼’’,‘‘的’’]B=1,L=4,vocab_size=1000,也就是每一个字对应词表里的1000token的对应概率,是一个长4宽1000的矩阵。

gengerate函数:防止上下文过大,设置温度,把握随机温度大小,影响随机性。取出最好一个token作为最后的概率来加入到序列之后(一般是特殊标志位,去这个标志位的概率作为预测,因为只有这个位置可以看清整句话)。

核心总结

输入: [B, L, D]  比如 [1, 4, 128] = 4个词,每个128维
         ↓
    ┌────┴────┐
    ↓         ↓
   Q投影     K投影    V投影
    ↓         ↓       ↓
  [1,4,128] [1,4,128] [1,4,128]
    ↓         ↓
   拆成4头    拆成4头   拆成4头
    ↓         ↓       ↓
  [1,4,4,32] [1,4,4,32] [1,4,4,32]
    ↓         ↓
   RoPE旋转   RoPE旋转
    ↓         ↓
   Q × Kᵀ → 匹配度矩阵 [1,4,4,4]
               ↓
          加上因果掩码(只看前面)
               ↓
          Softmax → 注意力权重
               ↓
          × V → 加权求和
               ↓
          合并多头 [1,4,128]
               ↓
          输出投影 [1,4,128]

完整代码

import torch
import torch.nn as nn
import torch.nn.functional as F

# -------------------------- 超参数设置 --------------------------
vocab_size = 1000    # 词表大小
dim = 128            # 模型维度
n_heads = 4          # 注意力头数
n_layers = 3         # Decoder层数
seq_len = 64         # 最大上下文长度
head_dim = dim // n_heads
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# -------------------------- RoPE 旋转位置编码(GPT主流) --------------------------
class RoPE(nn.Module):
    def __init__(self, dim):
        super(RoPE, self).__init__()
        self.dim = dim
        theta = 10000**(-torch.arange(0, dim, 2) / dim)#tensor([1.0000, 0.0100])
        self.register_buffer('theta', theta)#是 PyTorch 中用于将张量注册为模型的“缓冲区”(Buffer)
    def forward(self, x:torch.Tensor):
        #x=[B,H,L,D]
        B,H,L,D=x.shape
        pos=torch.arange(L,device=x.device)
        freqs=pos[:,None]*self.theta[None,:]
        cos = torch.cos(freqs)[None, None,:,:]
        sin = torch.sin(freqs)[None, None,:,:]
        x1, x2 = x.chunk(2, dim=-1)
        x_rot = torch.cat([x1*cos - x2*sin, x1*sin + x2*cos], dim = -1)
        return x_rot

# -------------------------- 多头因果自注意力(GPT专用,带掩码) --------------------------
class CausalSelfAttention(nn.Module):
    def __init__(self, dim,n_heads):
        super(CausalSelfAttention, self).__init__()
        self.dim = dim
        self.n_heads = n_heads
        self.head_dim = dim // n_heads
        self.rope=RoPE(self.head_dim)
        self.qkv_proj=nn.Linear(dim,dim*3)
        self.out_proj = nn.Linear(dim,dim)
        self.register_buffer("mask", torch.tril(torch.ones(seq_len, seq_len)) == 0)
    def forward(self, x:torch.Tensor):
        # x: [B, L, D]
        B, L, D = x.shape
        qkv=self.qkv_proj(x).split(D,dim=-1)
        q, k, v = [t.view(B, L, self.n_heads, self.head_dim).transpose(1, 2) for t in qkv]
        q=self.rope(q)
        k=self.rope(k)
        attn_score = q @ k.transpose(-2, -1) / (self.head_dim ** 0.5)
        attn_score.masked_fill_(self.mask[:L, :L], -1e9)
        attn_weight = F.softmax(attn_score, dim=-1)  # [B, H, L, L]
        out = attn_weight @ v  # [B, H, L, head_dim]
        out = out.transpose(1, 2).contiguous().view(B, L, D)  # 恢复为 [B, L, D]
        return self.out_proj(out)  # [B, L, D]

#----------------------------单层解码器---------------------------------
class DecoderLayer(nn.Module):
    def __init__(self, dim, heads):
        super(DecoderLayer, self).__init__()
        self.attn=CausalSelfAttention(dim,n_heads)
        self.ln1 = nn.LayerNorm(dim)
        self.ln2 = nn.LayerNorm(dim)
        self.ffn=nn.Sequential(
            nn.Linear(dim, dim*4),
            nn.GELU(),
            nn.Linear(dim*4, dim),
        )

    def forward(self, x):
        # Pre-Norm 架构(GPT标准)
        x = x + self.attn(self.ln1(x))  # 残差连接
        x = x + self.ffn(self.ln2(x))  # 残差连接
        return x

class GPT(nn.Module):
    def __init__(self):
        super(GPT, self).__init__()
        # 词嵌入:将 token ID 转为向量
        self.embedding = nn.Embedding(vocab_size, dim)
        #堆叠 3 层 Decoder
        self.decoder_blocks=nn.Sequential(
            *[DecoderLayer(dim,n_heads)
              for _ in range(n_heads)
              ]
        )
        # 最终归一化
        self.ln_final = nn.LayerNorm(dim)

        # 输出头:从向量映射回词表
        self.head = nn.Linear(dim, vocab_size)

    def forward(self, tokens):
        x = self.embedding(tokens)  # [B, L] → [B, L, D]
        x = self.decoder_blocks(x)  # [B, L, D] → [B, L, D]
        x = self.ln_final(x)
        logits = self.head(x)  # [B, L, vocab_size]
        return logits
    @torch.no_grad()
    def generate(self, prompt_tokens, max_new_tokens=20, temperature=0.7):
        self.eval()
        cur_tokens=prompt_tokens.to(device)
        for _ in range(max_new_tokens):
            if cur_tokens.shape[1]>seq_len:
                cur_tokens = cur_tokens[:, -seq_len:]
            logits = self(cur_tokens)
            next_logit = logits[:, -1, :] / temperature
            probs = F.softmax(next_logit, dim=-1)
            next_token = torch.multinomial(probs, num_samples=1)
            cur_tokens = torch.cat([cur_tokens, next_token], dim=-1)
        return cur_tokens
# -------------------------- 测试运行 --------------------------
if __name__ == "__main__":
    model = GPT().to(device)
    prompt = torch.tensor([[10, 20, 30]], device=device)
    print("输入token序列:", prompt.tolist())
    output = model.generate(prompt, max_new_tokens=20)
    print("生成后完整序列:", output.tolist())
    print(f"总长度:{output.shape[1]}")






更多推荐