Minigpt-底层逻辑理解与核心代码实现
参考:Building a Mini GPT from Scratch: Step-by-Step Guide to Understanding LLMs
原文链接[https://medium.com/@johirbuet/building-a-mini-gpt-from-scratch-step-by-step-guide-to-understanding-llms-02412f0b556f]
GPT:Generative Pre-trained Transformer
Generative:生成文本而不仅仅是分类
Pre-trained:大量文本数据+微调
Transformer:基于self-attention的神经网络架构
整体流程:
Text → Tokenizer → Token IDs → Embedding → [Transformer Block × N] → Linear → Softmax → 预测下一个 Token
输入: "I LOVE"
↓
Token IDs: [0, 1] # "I"=0, "LOVE"=1
↓
Embedding: [[0.2, -0.5, ...], # "I" 的向量 (1, 2, embed_dim)
[-0.1, 0.3, ...]] # "LOVE" 的向量
↓
Transformer Block × 12 (GPT-2 small)
↓
输出向量: [[..., ..., ...], # 蕴含上下文信息的表示
[..., ..., ...]]
↓
Linear (vocab_size=50257): 投影到词汇表维度
↓
Softmax: 得到每个词的概率分布
↓
预测: "MATH" (概率最高的词)
代码逻辑
MiniGPT (最外层)
- 管理整个模型生命周期
- 组合: Embedding + N×TransformerBlock
- 最终输出: Linear + Softmax
- 职责: "如何生成下一个 token?"
TransformerBlock (中间层) × N
- 组合: SelfAttention + FeedForward
- 职责: "提取一层上下文特征"
- 堆叠 N 层 = 深度特征提取
SelfAttention (最内层)
- 职责: "token 之间如何互相看"
- 只关心: Q, K, V 的计算和加权
1.turning text into tokens
sentence → tokens(a numerical language)
text="I LOVE MATH"
vocab = {'I':0,'LOVE':1,'MATH':2}
tokens = [0,1,2]
2.embeddings
tokens → vector
vocab_size = 3
embed_dim=4
# 创建一个可学习的embedding层
embedding = nn.Embedding(vocab_size,embed_dim)
# 输入为tensor.size(3),输出为tensor.size(3,dim)
embedded = embedding(torch.tensor(tokens))
print("=== 嵌入层的权重矩阵 ===")
print(f"形状: {embedding.weight.shape}")
print(embedding.weight)
# 输出示例(随机初始化):
# tensor([[ 0.3367, 0.1288, 0.2345, 0.2303],
# [-0.0328, -0.5062, -0.5234, 0.7175],
# [-0.3835, 0.6445, -0.1614, -0.4310]])
print("\n=== 输入 tokens ===")
print(f"tokens: {tokens}")
# tokens: [0, 1, 2]
print("\n=== 输出嵌入向量 ===")
print(f"形状: {embedded.shape}")
print(embedded)
# 输出:
# tensor([[ 0.3367, 0.1288, 0.2345, 0.2303],
# [-0.0328, -0.5062, -0.5234, 0.7175],
# [-0.3835, 0.6445, -0.1614, -0.4310]],
# grad_fn=<EmbeddingBackward0>)
nn.Embedding本质上是一个 可学习的查找表(Lookup Table),内部维护一个形状为 (vocab_size, embed_dim) 的权重矩阵
embed_dim嵌入维度是一个超参数,取决于任务需求、模型规模和计算资源,决定了每个token用多少维的向量来表示,维度越大表达能力越强,但计算开销也越大。小任务128256,中等任务512768,大模型1024+
embed_dim=4
┌─────────────────────┐
vocab_size=3│ [0.2, -0.5, 1.0, 0.1] ← token 0 ("I") 的向量
│ [-0.1, 0.3, 0.8, -0.4] ← token 1 ("LOVE") 的向量
│ [0.5, -0.2, 0.1, 0.9] ← token 2 ("MATH") 的向量
└─────────────────────┘
转化过程
输入 tokens: [0, 1, 2]
↓
在权重矩阵中按行索引查找
↓
输出 vectors: [
[0.2, -0.5, 1.0, 0.1], # token 0 对应的第 0 行
[-0.1, 0.3, 0.8, -0.4], # token 1 对应的第 1 行
[0.5, -0.2, 0.1, 0.9] # token 2 对应的第 2 行
]
关键理解:
1)tokens不是被计算成vector的,而是被“查找”成vector的;
2)vectors为随机初始化向量,在训练过程中通过反向传播自动学习
另:原理类似于one-hot编码,两者在数学上等价,但由于one-hot需要存储大量稀疏矩阵,而embedding直接查表,内存效率高、计算快
tokens仅仅是数字,模型需要learn的是每个token是何含义,同样语序也很重要
word_embedding + position_embedding → input to attention
- embedding详解[https://zhuanlan.zhihu.com/p/1916927561000255869]
3.self-attention
每个token都可以看到其他token,然后根据相关性加权融合信息
class SelfAttention(nn.Module):
def __init__(self, embed_dim):
super().__init__()
# self.qkv_proj = nn.Linear(embed_dim, embed_dim * 3)
self.query = nn.Linear(embed_dim, embed_dim)
self.key = nn.Linear(embed_dim, embed_dim)
self.value = nn.Linear(embed_dim, embed_dim)
self.scale = embed_dim ** 0.5
def forward(self, x):
# qkv = self.qkv_proj(x)
# Q, K, V = QKV.split(embed_dim, dim=-1)
Q = self.query(x)
K = self.key(x)
V = self.value(x)
scores = (Q @ K.transpose(-2, -1)) / self.scale
weights = torch.softmax(scores, dim=-1)
return weights @ V # 加权求和
attention的输出为融合上下文信息的新向量
另:gpt的掩码机制确保gpt只能看到前面的token
输入: "I LOVE MATH"
↓
Attention 前: 每个词只知道自己
"I" → [0.2, -0.5, ...] # 只有 "I" 的语义
"LOVE" → [-0.1, 0.3, ...] # 只有 "LOVE" 的语义
"MATH" → [0.5, -0.2, ...] # 只有 "MATH" 的语义
Attention 后: 每个词融合了上下文
"I" → [0.2, -0.5, ...] # 变化不大(第一个词,只能看自己)
"LOVE" → [0.1, 0.8, ...] # 融合了 "I" 的信息,知道主语是 "I"
"MATH" → [0.6, 0.1, ...] # 融合了 "I LOVE" 的信息,知道是"我爱数学"
4.Transformer blocks
多层堆叠,提取深层次特征
Attention做的是线性组合(加权求和),FeedForward做的是非线性变换
class miniTransformerBlock(nn.Module):
def __init__(self,embed_dim):
super().__init__()
self.attn = SelfAttention(embed_dim)
self.ff = nn.Sequential(
nn.Linear(embed_dim,embed_dim*4) # 升维
nn.ReLU() # 激活
nn.Linear(embed_dim*4,embed_dim) # 降维
)
self.norm1 = nn.LayerNorm(embed_dim)
self.norm2 = nn.LayerNorm(embed_dim)
def forward(self,x):
x = x+self.attn(self.norm1(x))
x = x+self.ff(self.norm2(x))
return x
transformer block内部流程
输入 x
│
├─→ [SelfAttention] ──→ attn_out
│ │
│ ↓
│ x + attn_out ← 残差连接(保留原始信息)
│ │
│ ↓
│ LayerNorm
│ │
├─→ [FeedForward] ──→ ff_out
│ │
│ ↓
│ x + ff_out ← 残差连接
│ │
│ ↓
│ LayerNorm
│ │
└──────────────────────→ 输出 x'(上下文增强的向量表示)
可以理解为
Block 1: 提取"相邻词关系"
"I" → "LOVE" 是主谓关系
Block 2-3: 提取"短语级别关系"
"LOVE MATH" → 是一个动宾短语
Block 4-6: 提取"句子级别语义"
"I LOVE MATH" → 表达积极情感,与 STEM 相关
Block 7-9: 提取"跨句子关系"(如果有多个句子)
与上文提到的"学校"相关 → 推测是学生
Block 10-12: 提取"世界知识"
"MATH" 与 "ALGEBRA", "GEOMETRY" 相关
可能用于预测下一个词 "AND", "BECAUSE", "CLASS" 等
关于特征提取类比图像特征提取的层次
| 层数 | 图像 CNN | 文本 Transformer |
|---|---|---|
| 浅层 | 边缘、颜色 | 词性、基本语法 |
| 中层 | 纹理、形状 | 短语、局部语义 |
| 深层 | 物体部件 | 句子语义、情感 |
| 更深层 | 完整物体 | 世界知识、推理 |
针对不同问题采取的解决方案
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 梯度消失/爆炸 | 深层网络训不动 | 残差连接 |
| 数值不稳定 | 激活值越来越大 | 层归一化 |
| 注意力分数爆炸 | Q·K^T 数值太大 | 除以 √d_k |
| 过拟合 | 训练好但测试差 | Dropout |
5.The language modeling head
考虑如何生成下一个token
class MiniGPT(nn.Module):
def __init__(self,vocab_size,embed_dim):
super().__init__()
self.embedding = nn.Embedding(vocab_size,embed_dim)
self.pos_embedding = nn.Embeding(10,embed_dim)
self.transformer = miniTransformerBlock(embed_dim)
# 将语义向量投影到词汇表维度
self.lm_head = nn.Linear(embed_dim, vocab_size)
def forward(self,tokens):
positions = torch.arange(len(tokens))
x = self.embedding(tokens)+self.pos_embedding(positions)
x = self.transformer(X)
logits = self.lm_head(x)
return logits
lm_head从语义空间到词汇表空间
x' # 融合上下文的向量
│
├─→ nn.Linear(embed_dim, vocab_size) # 关键:投影到词汇表维度!
│
└─→ logits # 每个位置对每个词的"分数"
举例说明
vocab_size = 50257 # GPT-2 的词汇表大小
每个位置需要输出一个概率分布:
"下一个词是词汇表中第 0 个词的概率"
"下一个词是词汇表中第 1 个词的概率"
...
"下一个词是词汇表中第 50256 个词的概率"
所以要投影到 vocab_size 维,Transformer 输出的是"上下文融合后的语义向量",lm_head 把这个向量"翻译"成词汇表上的概率分布。取最后一个位置的分布,就是"下一个词"的预测。
6.Training
With enough data and time, it learns grammar, rhythm, and even meaning.
以交叉熵损失函数为例,伪代码如下
for tokens in dataset:
logits = model(tokens[:-1]) # 根据最后一个位置的概率预测下一个词
loss = cross_entropy(logits, tokens[1:])
loss.backward()
optimizer.step()
训练时每个位置都预测下一个词,
输入: [I, LOVE, MATH]
目标: [LOVE, MATH, !]
7.Inference-generating
After training, generation is simple:
- Feed the model a few starting tokens
- Sample the next one from its output probabilities
- Append it to the input and repeat
def generate(model, start, max_new_tokens=10):
tokens = start
for _ in range(max_new_tokens):
logits = model(tokens)
next_token = torch.argmax(logits[-1], dim=-1)
tokens = torch.cat([tokens, next_token.unsqueeze(0)])
return tokens
(待补充
更多学习资料:
【LLM】从零到一构建一个小型LLM--MiniGPT - 详解https://www.cnblogs.com/wzzkaifa/p/19010059
本文来自博客园,作者:BlnoOo,转载请注明原文链接:https://www.cnblogs.com/Bolan/p/21657537

浙公网安备 33010602011771号