MicroGPT解读&思考

Andrej Karpathy 的 MicroGPT 展示了在已有的真实名字中学习规律,并通过优化训练编出新名字的过程,揭示了AI大模型训练的底层逻辑

MicroGPT解读&思考

Andrej Karpathy 的 MicroGPT 展示了在已有的真实名字中学习规律,并通过优化训练编出新名字的过程,揭示了AI大模型训练的底层逻辑。

一、项目概述

Andrej Karpathy 的 MicroGPT 项目以极简的代码(200余行)构建出了AI大模型训练的底层架构,涵盖了从数据准备、自定义自动微分引擎(Value 类)、Transformer 单层架构(多头注意力、MLP、RMSNorm)、Adam 优化器到训练与推理的完整流程。

项目以名字生成为例,从真实名字数据集中学习字符分布,训练后能够自主生成符合语言规律的新名字。

Karpathy's post

项目代码(文末附中文注释版):加载链接预览…

省流:

  1. 准备名字列表:下载名字列表,打乱,建立字母表。
  2. 造大脑:初始化许多小旋钮(参数),每个旋钮是一个小纸条(Value),能记录数字和计算关系。
  3. 定义思考方式:写函数 gpt,描述大脑如何根据当前字母和位置,利用记忆(keys/values)推测下一个字母。
  4. 训练:反复看名字,每看一个名字,让大脑猜,算猜错的程度(损失),然后通过小纸条的反向传播算出每个旋钮该往哪个方向拧一点,再用 Adam 方法拧动旋钮。这样训练的效果会越来越好。
  5. 创作:训练完,让大脑自己一个字一个字地编名字,打印出来看看它学会了没有。

二、项目构建

2.1 导入工具包

import os       # 用于检查文件是否存在
import math     # 用于数学计算(对数、指数等)
import random   # 用于生成随机数、打乱顺序等
random.seed(42) # 固定随机种子,让每次运行结果一样

2.2 获取名字列表

电脑先从网上下载一个名字列表,里面有很多真实的名字,每行一个。

# 如果当前目录没有 input.txt 文件,就从网上下载名字列表
if not os.path.exists('input.txt'):
    import urllib.request
    names_url = 'https://raw.githubusercontent.com/karpathy/makemore/refs/heads/master/names.txt'
    urllib.request.urlretrieve(names_url, 'input.txt')

2.3 读取文件并打乱顺序

读取全部名字然后打乱顺序,这样它就不会只盯着前几个名字学,而是随机看,学得更全面。

# 读取文件,按行分割,去掉空行和首尾空格,得到名字列表 docs
docs = [l.strip() for l in open('input.txt').read().strip().split('\n') if l.strip()]
random.shuffle(docs)  # 打乱名字顺序
print(f"名字总数: {len(docs)}")

三、定义字母

3.1 构建字母表

名字都是由字母组成的。电脑需要先知道它要学哪些字母,因此需要把所有的名字拼在一起,找出所有不同的字母(比如 a,b,c,…,A,B,C…),然后给每个字母编一个号(比如 a=0,b=1,c=2……),这样电脑就能用数字来代表字母了。

# 找出所有不重复的字符,排序后作为字母表
uchars = sorted(set(''.join(docs)))

3.2 添加符号并定义表大小

另外还需要一个特殊的“开始”符号(类似作文开头空两格)。电脑看到这个符号,就知道“名字要开始了”。这个符号也编一个号,比如 26(如果前面字母有 0~25 的话)。

# 定义特殊的“开始”符号 BOS,编号为字母表长度
BOS = len(uchars)
# 词汇表大小 = 字母数量 + BOS
vocab_size = len(uchars) + 1
print(f"词汇表大小: {vocab_size}")

现在,电脑的“词汇表”里一共有:所有字母 + 开始符号。以后电脑猜下一个字母,就是从这些里面选一个。

四、造一个“大脑”

电脑要学习,得有一个“大脑”。

这个大脑里有很多很多小旋钮(可以想象成收音机上的调频旋钮)。

一开始,这些小旋钮都是随便转到一个位置的,所以大脑什么也不会。

大脑的任务是:看到当前字母和它在名字里的位置(比如第几个字),然后猜下一个字母是什么。

猜的时候,它会用到这些旋钮,把当前字母和位置变成一些数字(可以叫做“想法”),再经过一些计算,最后从词汇表里选一个字母作为答案。

4.1-4.5 内容较为复杂,只为了解逻辑可跳过。

4.1 定义小纸条

初始化节点,存储数值、梯度、子节点和局部导数;重载加法运算;重载乘法运算;重载幂运算(指数为常数);定义对数运算;定义指数运算;定义 ReLU 激活函数;定义其他运算(负数、减法、除法等);反向传播(计算梯度)。

# 定义自动微分的小纸条类 Value
class Value:
    """存储一个标量值和它的梯度,作为计算图中的一个节点"""
    def __init__(self, data, children=(), local_grads=()):
        self.data = data                # 前向计算得到的数值
        self.grad = 0                   # 损失对该节点的梯度,反向传播时计算
        self._children = children       # 生成该节点所依赖的子节点
        self._local_grads = local_grads # 该节点对每个子节点的局部导数
    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return Value(self.data + other.data, (self, other), (1, 1))
    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return Value(self.data * other.data, (self, other), (other.data, self.data))
    def __pow__(self, other):
        return Value(self.data ** other, (self,), (other * self.data ** (other - 1),))
    def log(self):
        return Value(math.log(self.data), (self,), (1 / self.data,))
    def exp(self):
        return Value(math.exp(self.data), (self,), (math.exp(self.data),))
    def relu(self):
        return Value(max(0, self.data), (self,), (float(self.data > 0),))
    def __neg__(self):
        return self * -1
    def __radd__(self, other):
        return self + other
    def __sub__(self, other):
        return self + (-other)
    def __rsub__(self, other):
        return other + (-self)
    def __rmul__(self, other):
        return self * other
    def __truediv__(self, other):
        return self * other ** -1
    def __rtruediv__(self, other):
        return other * self ** -1
    def backward(self):
        # 拓扑排序,得到计算顺序
        topo = []
        visited = set()
        def build_topo(v):
            if v not in visited:
                visited.add(v)
                for child in v._children:
                    build_topo(child)
                topo.append(v)
        build_topo(self)
        # 从当前节点开始反向传播
        self.grad = 1
        for v in reversed(topo):
            for child, local_grad in zip(v._children, v._local_grads):
                child.grad += local_grad * v.grad

4.2 设定大脑的规格

n_embd = 16      # 每个字母用16个数字表示(嵌入维度)
n_head = 4       # 注意力头的数量
n_layer = 1      # 层数(这里只用一层)
block_size = 8   # 最大序列长度
head_dim = n_embd // n_head  # 每个注意力头负责的维度

4.3 辅助函数

创建随机初始化的矩阵,每个元素是一个小纸条。

# 辅助函数:创建一个矩阵,每个元素是一个服从高斯分布的小纸条
matrix = lambda nout, nin, std=0.02: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)]

4.4 初始化模型参数

模型参数即为大脑里的各种表格。

state_dict = {
    'wte': matrix(vocab_size, n_embd),       # 字母特征表
    'wpe': matrix(block_size, n_embd),       # 位置特征表
    'lm_head': matrix(vocab_size, n_embd),   # 输出层
}
for i in range(n_layer):
    state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd)          # 注意力 query 投影矩阵
    state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd)          # 注意力 key 投影矩阵
    state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd)          # 注意力 value 投影矩阵
    state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd, std=0)   # 注意力输出投影矩阵(初始化为0)
    state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd)      # MLP 第一层
    state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd, std=0) # MLP 第二层(初始化为0)
# 把所有参数(小纸条)展平到一个列表里,方便优化
params = [p for mat in state_dict.values() for row in mat for p in row]
print(f"参数总数: {len(params)}")

4.5 定义思考方式

定义大脑的思考方式,即模型前向传播函数。

def linear(x, w):
    """线性层:输入向量x,权重矩阵w,输出x与w每行的点积"""
    return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w]
def softmax(logits):
    """将分数转换为概率分布"""
    max_val = max(val.data for val in logits)
    exps = [(val - max_val).exp() for val in logits]
    total = sum(exps)
    return [e / total for e in exps]
def rmsnorm(x):
    """RMSNorm 归一化"""
    ms = sum(xi * xi for xi in x) / len(x)
    scale = (ms + 1e-5) ** -0.5
    return [xi * scale for xi in x]
def gpt(token_id, pos_id, keys, values):
    """GPT 模型的前向传播"""
    # 取出当前字母的特征和位置特征
    tok_emb = state_dict['wte'][token_id]
    pos_emb = state_dict['wpe'][pos_id]
    x = [t + p for t, p in zip(tok_emb, pos_emb)]  # 相加得到综合表示
    x = rmsnorm(x)
    for li in range(n_layer):
        # 1) 多头注意力块
        x_residual = x
        x = rmsnorm(x)
        q = linear(x, state_dict[f'layer{li}.attn_wq'])
        k = linear(x, state_dict[f'layer{li}.attn_wk'])
        v = linear(x, state_dict[f'layer{li}.attn_wv'])
        # 将当前 key 和 value 存入缓存
        keys[li].append(k)
        values[li].append(v)
        x_attn = []
        for h in range(n_head):
            hs = h * head_dim
            q_h = q[hs:hs + head_dim]
            k_h = [ki[hs:hs + head_dim] for ki in keys[li]]
            v_h = [vi[hs:hs + head_dim] for vi in values[li]]
            # 计算注意力分数
            attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim ** 0.5
                           for t in range(len(k_h))]
            attn_weights = softmax(attn_logits)
            # 加权求和得到头输出
            head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim)]
            x_attn.extend(head_out)
        x = linear(x_attn, state_dict[f'layer{li}.attn_wo'])
        x = [a + b for a, b in zip(x, x_residual)]  # 残差连接
        # 2) MLP 块
        x_residual = x
        x = rmsnorm(x)
        x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
        x = [xi.relu() ** 2 for xi in x]           # ReLU² 激活
        x = linear(x, state_dict[f'layer{li}.mlp_fc2'])
        x = [a + b for a, b in zip(x, x_residual)]  # 残差连接
    # 输出层,得到词汇表大小的分数
    logits = linear(x, state_dict['lm_head'])
    return logits

五、训练大脑

拿一个名字:比如 “Emma”。电脑先把它变成数字:E→4,m→12,m→12,a→0。

然后在开头和结尾加上“开始”符号(比如 26)。 最后得到:[26, 4, 12, 12, 0, 26]。

让大脑猜: 先看第一个符号 26(开始),大脑要猜下一个字母是谁。正确答案是 4(E)。

如果大脑猜对了,就表扬;猜错了,就告诉它“你猜错了,正确答案是 E”。

然后看 26 和 4,大脑要猜再下一个字母,正确答案是 12(m)。

依此类推,一直猜到最后一个字母,大脑要猜结束符号 26。

调整旋钮:每猜完一个名字,电脑就会根据大脑猜得对不对,稍微转动一下那些小旋钮。 转动的方向是:如果猜错了,就朝能猜对的方向转一点点。

这样,下次再看类似的名字时,大脑就会更接近正确答案。 重复练习:电脑不停地拿新的名字,一个一个地猜,然后调整旋钮。

总共练习 500 次(代码里的 500 步)。

每次练习完,电脑都会打印一个数字(损失),这个数字越小,说明大脑猜得越准。 这个数字会越来越小,说明大脑正在学习进步。

以下为相关代码,只为了解逻辑可跳过。

5.1 设置优化器及缓存

  • learning_rate = 0.01:初始学习率,控制每次调整的步长。
  • beta1 = 0.9beta2 = 0.95:两个记忆系数,决定记住多少历史信息。
  • eps_adam = 1e-8:一个很小的数,防止除零。
  • m 和 v 是两个记忆列表,长度和参数个数一样,初始全 0,用来存储每个参数的“一阶动量”(梯度的平均)和“二阶动量”(梯度平方的平均)。
# Adam 优化器参数
learning_rate, beta1, beta2, eps_adam = 1e-2, 0.9, 0.95, 1e-8
m = [0.0] * len(params)  # 一阶动量缓存
v = [0.0] * len(params)  # 二阶动量缓存

5.2 开始训练循环

设定训练循环为500步。

num_steps = 500  # 训练步数
for step in range(num_steps):

拿一个名字,转换为数字列表,首尾加上 BOS。

    # 取一个名字,转换为数字列表,首尾加上 BOS
    doc = docs[step % len(docs)]
    tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
    n = min(block_size, len(tokens) - 1)  # 有效预测长度

初始化每层的记忆缓存和损失列表。

    keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
    losses = []

让大脑逐个位置猜下一个字母。

    # 对每个位置进行预测
    for pos_id in range(n):
        token_id, target_id = tokens[pos_id], tokens[pos_id + 1]
        logits = gpt(token_id, pos_id, keys, values)
        probs = softmax(logits)
        loss_t = -probs[target_id].log()  # 负对数似然损失
        losses.append(loss_t)

计算平均损失。

    # 平均损失
    loss = (1 / n) * sum(losses)

反向传播,计算所有参数的梯度。

    # 反向传播,计算梯度
    loss.backward()

用余弦退火计算当前步的学习率,这样模型会逐步趋于稳定。

    # 余弦退火学习率
    lr_t = learning_rate * 0.5 * (1 + math.cos(math.pi * step / num_steps))

用 Adam 优化器更新所有参数(转动小旋钮)。

    # 用 Adam 更新所有参数
    for i, p in enumerate(params):
        m[i] = beta1 * m[i] + (1 - beta1) * p.grad
        v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2
        m_hat = m[i] / (1 - beta1 ** (step + 1))
        v_hat = v[i] / (1 - beta2 ** (step + 1))
        p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
        p.grad = 0  # 梯度清零

打印当前步数和损失。

    print(f"步数 {step + 1:4d} / {num_steps:4d} | 损失 {loss.data:.4f}")

六、输出结果

训练 500 次之后,大脑已经学得差不多了,现在可以让它自己编名字。

开始:给大脑一个“开始”符号。大脑根据“开始”符号,猜第一个字母是谁。它不会直接选最可能的那一个,而是随机选,但猜对概率高的字母更容易被选到(这叫“有点创意,但又不乱来”)。

6.1 设置温度参数

代码里有个“温度”参数,温度低就保守(选最可能那个),温度高就爱冒险(可能选冷门的字母)。

temperature = 0.5  # 温度参数,控制随机性

6.2 生成并打印样本

继续猜:把猜到的字母作为新的当前字母,继续猜下一个。

这样一字一字往下猜,直到大脑猜出“结束”符号,或者猜够了 8 个字母(因为名字一般不会太长)。

看结果:电脑会编出 20 个新名字,打印出来。

print("\n--- 推理生成 ---")
for sample_idx in range(20):
    keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
    token_id = BOS
    sample = []
    for pos_id in range(block_size):
        logits = gpt(token_id, pos_id, keys, values)
        # 温度调整
        probs = softmax([l / temperature for l in logits])
        # 按概率随机采样下一个 token
        token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
        if token_id == BOS:
            break
        sample.append(uchars[token_id])
    print(f"样本 {sample_idx + 1:2d}: {''.join(sample)}")

七、理解与思考

虽然 MicroGPT 是一个只有单层 Transformer、16 维嵌入、500 步训练的微型模型,但它与当今前沿的大模型有着完全相同的核心架构和训练方式。

MicroGPT 剥去了深度学习框架的封装,直接展示了 Transformer 的每一行数学运算:线性层(linear)就是矩阵乘法,注意力机制就是查询(Q)与键(K)的点积缩放再加权求和,无论多大的模型,其核心只是这些基本操作的组合与堆叠。

由此可见,大模型的本质是“矩阵运算 + 非线性”。

虽然 MicroGPT 规模极小,但它的设计可以无缝扩展到更大的规模(增加层数、维度、数据量)。这样我们就理解了 OpenAI 等企业将 Transformer 扩展到千亿参数的基本原理——无非是“更多的层、更多的头、更多的数据、更强的算力”,而核心逻辑保持不变。

八、完整代码(注释)

# 第一步:给电脑看名字

import os       # 用于检查文件是否存在
import math     # 用于数学计算(对数、指数等)
import random   # 用于生成随机数、打乱顺序等
random.seed(42) # 固定随机种子,让每次运行结果一样

# 如果当前目录没有 input.txt 文件,就从网上下载名字列表
if not os.path.exists('input.txt'):
    import urllib.request
    names_url = 'https://raw.githubusercontent.com/karpathy/makemore/refs/heads/master/names.txt'
    urllib.request.urlretrieve(names_url, 'input.txt')

# 读取文件,按行分割,去掉空行和首尾空格,得到名字列表 docs
docs = [l.strip() for l in open('input.txt').read().strip().split('\n') if l.strip()]
random.shuffle(docs)  # 打乱名字顺序
print(f"名字总数: {len(docs)}")

# 第二步:让电脑认识字母

# 找出所有不重复的字符,排序后作为字母表
uchars = sorted(set(''.join(docs)))
# 定义特殊的“开始”符号 BOS,编号为字母表长度
BOS = len(uchars)
# 词汇表大小 = 字母数量 + BOS
vocab_size = len(uchars) + 1
print(f"词汇表大小: {vocab_size}")

# 第三步:给电脑造一个“大脑”

# 定义自动微分的小纸条类 Value
class Value:
    """存储一个标量值和它的梯度,作为计算图中的一个节点"""

    def __init__(self, data, children=(), local_grads=()):
        self.data = data                # 前向计算得到的数值
        self.grad = 0                   # 损失对该节点的梯度,反向传播时计算
        self._children = children       # 生成该节点所依赖的子节点
        self._local_grads = local_grads # 该节点对每个子节点的局部导数

    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return Value(self.data + other.data, (self, other), (1, 1))

    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return Value(self.data * other.data, (self, other), (other.data, self.data))

    def __pow__(self, other):
        return Value(self.data ** other, (self,), (other * self.data ** (other - 1),))

    def log(self):
        return Value(math.log(self.data), (self,), (1 / self.data,))

    def exp(self):
        return Value(math.exp(self.data), (self,), (math.exp(self.data),))

    def relu(self):
        return Value(max(0, self.data), (self,), (float(self.data > 0),))

    def __neg__(self):
        return self * -1

    def __radd__(self, other):
        return self + other

    def __sub__(self, other):
        return self + (-other)

    def __rsub__(self, other):
        return other + (-self)

    def __rmul__(self, other):
        return self * other

    def __truediv__(self, other):
        return self * other ** -1

    def __rtruediv__(self, other):
        return other * self ** -1

    def backward(self):
        # 拓扑排序,得到计算顺序
        topo = []
        visited = set()

        def build_topo(v):
            if v not in visited:
                visited.add(v)
                for child in v._children:
                    build_topo(child)
                topo.append(v)

        build_topo(self)

        # 从当前节点开始反向传播
        self.grad = 1
        for v in reversed(topo):
            for child, local_grad in zip(v._children, v._local_grads):
                child.grad += local_grad * v.grad

# 设定大脑的规格
n_embd = 16      # 每个字母用16个数字表示(嵌入维度)
n_head = 4       # 注意力头的数量
n_layer = 1      # 层数(这里只用一层)
block_size = 8   # 最大序列长度
head_dim = n_embd // n_head  # 每个注意力头负责的维度

# 辅助函数:创建一个矩阵,每个元素是一个服从高斯分布的小纸条
matrix = lambda nout, nin, std=0.02: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)]

# 初始化模型参数(大脑里的各种表格)
state_dict = {
    'wte': matrix(vocab_size, n_embd),       # 字母特征表
    'wpe': matrix(block_size, n_embd),       # 位置特征表
    'lm_head': matrix(vocab_size, n_embd),   # 输出层
}

for i in range(n_layer):
    state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd)          # 注意力 query 投影矩阵
    state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd)          # 注意力 key 投影矩阵
    state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd)          # 注意力 value 投影矩阵
    state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd, std=0)   # 注意力输出投影矩阵(初始化为0)
    state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd)      # MLP 第一层
    state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd, std=0) # MLP 第二层(初始化为0)

# 把所有参数(小纸条)展平到一个列表里,方便优化
params = [p for mat in state_dict.values() for row in mat for p in row]
print(f"参数总数: {len(params)}")

# 定义大脑的思考方式(模型前向传播)

def linear(x, w):
    """线性层:输入向量x,权重矩阵w,输出x与w每行的点积"""
    return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w]

def softmax(logits):
    """将分数转换为概率分布"""
    max_val = max(val.data for val in logits)
    exps = [(val - max_val).exp() for val in logits]
    total = sum(exps)
    return [e / total for e in exps]

def rmsnorm(x):
    """RMSNorm 归一化"""
    ms = sum(xi * xi for xi in x) / len(x)
    scale = (ms + 1e-5) ** -0.5
    return [xi * scale for xi in x]

def gpt(token_id, pos_id, keys, values):
    """GPT 模型的前向传播"""
    # 取出当前字母的特征和位置特征
    tok_emb = state_dict['wte'][token_id]
    pos_emb = state_dict['wpe'][pos_id]
    x = [t + p for t, p in zip(tok_emb, pos_emb)]  # 相加得到综合表示
    x = rmsnorm(x)

    for li in range(n_layer):
        # 1) 多头注意力块
        x_residual = x
        x = rmsnorm(x)

        q = linear(x, state_dict[f'layer{li}.attn_wq'])
        k = linear(x, state_dict[f'layer{li}.attn_wk'])
        v = linear(x, state_dict[f'layer{li}.attn_wv'])

        # 将当前 key 和 value 存入缓存
        keys[li].append(k)
        values[li].append(v)

        x_attn = []
        for h in range(n_head):
            hs = h * head_dim
            q_h = q[hs:hs + head_dim]
            k_h = [ki[hs:hs + head_dim] for ki in keys[li]]
            v_h = [vi[hs:hs + head_dim] for vi in values[li]]

            # 计算注意力分数
            attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim ** 0.5
                           for t in range(len(k_h))]
            attn_weights = softmax(attn_logits)

            # 加权求和得到头输出
            head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim)]
            x_attn.extend(head_out)

        x = linear(x_attn, state_dict[f'layer{li}.attn_wo'])
        x = [a + b for a, b in zip(x, x_residual)]  # 残差连接

        # 2) MLP 块
        x_residual = x
        x = rmsnorm(x)
        x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
        x = [xi.relu() ** 2 for xi in x]           # ReLU² 激活
        x = linear(x, state_dict[f'layer{li}.mlp_fc2'])
        x = [a + b for a, b in zip(x, x_residual)]  # 残差连接

    # 输出层,得到词汇表大小的分数
    logits = linear(x, state_dict['lm_head'])
    return logits

# 第四步:教大脑学习(训练)

# Adam 优化器参数
learning_rate, beta1, beta2, eps_adam = 1e-2, 0.9, 0.95, 1e-8
m = [0.0] * len(params)  # 一阶动量缓存
v = [0.0] * len(params)  # 二阶动量缓存

num_steps = 500  # 训练步数
for step in range(num_steps):
    # 取一个名字,转换为数字列表,首尾加上 BOS
    doc = docs[step % len(docs)]
    tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
    n = min(block_size, len(tokens) - 1)  # 有效预测长度

    # 初始化每层的记忆缓存和损失列表
    keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
    losses = []

    # 对每个位置进行预测
    for pos_id in range(n):
        token_id, target_id = tokens[pos_id], tokens[pos_id + 1]
        logits = gpt(token_id, pos_id, keys, values)

        probs = softmax(logits)
        loss_t = -probs[target_id].log()  # 负对数似然损失
        losses.append(loss_t)

    # 平均损失
    loss = (1 / n) * sum(losses)

    # 反向传播,计算梯度
    loss.backward()

    # 余弦退火学习率
    lr_t = learning_rate * 0.5 * (1 + math.cos(math.pi * step / num_steps))

    # 用 Adam 更新所有参数
    for i, p in enumerate(params):
        m[i] = beta1 * m[i] + (1 - beta1) * p.grad
        v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2
        m_hat = m[i] / (1 - beta1 ** (step + 1))
        v_hat = v[i] / (1 - beta2 ** (step + 1))
        p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
        p.grad = 0  # 梯度清零

    print(f"步数 {step + 1:4d} / {num_steps:4d} | 损失 {loss.data:.4f}")

# 第五步:让电脑自己编名字(推理)

temperature = 0.5  # 温度参数,控制随机性
print("\n--- 推理生成 ---")
for sample_idx in range(20):
    keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
    token_id = BOS
    sample = []

    for pos_id in range(block_size):
        logits = gpt(token_id, pos_id, keys, values)
        # 温度调整
        probs = softmax([l / temperature for l in logits])
        # 按概率随机采样下一个 token
        token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
        if token_id == BOS:
            break
        sample.append(uchars[token_id])

    print(f"样本 {sample_idx + 1:2d}: {''.join(sample)}")

KEEP READING