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

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

posted @ 2026-07-20 00:05  BlnoOo  阅读(4)  评论(0)    收藏  举报