基于极简demo,深入浅出理解GPT模型架构
引言
针对新手小白,一开始入门大模型面对的很多困难,许多晦涩的概念一时半会也理解不了,同时网上的资料五花八门,一方面这是我第一次写,想帮助像我一样迷茫的小白,另一方面也想借此梳理学到的东西,仅仅作为入门的文章,如有不足,还望海涵。
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]}")
更多推荐



所有评论(0)