
1. 从一行 NumPy 说起为什么程序员要啃 LLM 这块硬骨头刚入行那会儿我对 NumPy 的全部认知就是“一个比 list 快很多的数组库”。写个np.array([1,2,3])做个矩阵乘法顶多再算个均值方差感觉已经摸到了天花板。直到后来真正开始接触大模型才发现自己当年那点 NumPy 功底其实正是理解 LLM 的第一块敲门砖。你可能会问NumPy 和大模型之间隔着十万八千里吧一个是科学计算的基础库一个是动辄几百亿参数的庞然大物能有什么关系关系大了去了。大模型说到底就是一堆张量在流动。你输入的每一句话先被切成 token再映射成向量然后经过几十层 Transformer 的加工每一层里全是矩阵乘法、加法、归一化、激活函数。这些操作在底层实现上和你在 NumPy 里做的np.dot、np.matmul、np.exp没有本质区别只是规模从几十个元素变成了几百万个元素并且跑在了 GPU 上。所以我的学习路径很明确先把 NumPy 里那些数组操作、广播机制、shape 变换彻底搞明白再去理解 Transformer 的架构最后动手跑通一个小规模的 LLM整个过程会顺畅很多。这篇笔记适合谁看如果你是 Python 开发者用过 NumPy 但没碰过深度学习或者你尝试过读 Transformer 论文但被各种矩阵维度绕晕了又或者你想自己跑一个开源大模型但不知道从哪下手那这篇内容应该能帮到你。我不会堆砌公式也不会一上来就甩一堆论文链接而是按照我自己踩过的坑从最基础的数组操作开始一步步把 LLM 的核心逻辑拆开讲清楚。你不需要有 GPU 集群一台普通笔记本就能跟着走完大部分流程。2. NumPy 基础回顾那些你以为是“常识”但可能没吃透的东西2.1 从 list 到 ndarray快在哪里很多人知道 NumPy 比 Python list 快但说不清楚为什么快。我一开始也这样直到有次面试被问到“NumPy 和 list 比快在哪”才认真去查了底层实现。简单说Python list 存的是对象的指针每个元素都是一个完整的 Python 对象有类型信息、引用计数、内存地址。你做一个循环加法每次都要做类型检查、拆箱、运算、装箱开销巨大。而 NumPy 的 ndarray 是一块连续的内存里面存的是同一种数据类型的原始值比如 float64 就是 8 个字节挨着排。做加法时底层直接用 C 循环遍历这块内存没有 Python 解释器的介入速度自然快几十倍甚至上百倍。你可以自己测一下创建一个一百万元素的 list 和一个一百万元素的 ndarray分别做求和用timeit跑一下差距非常直观。我实测下来NumPy 的求和通常比纯 Python 循环快 50 到 100 倍如果用了向量化操作而不是循环差距还会更大。这个“向量化”思维就是后面理解大模型计算的关键。大模型里没有人在 Python 层面写 for 循环去算每个神经元的输出全是矩阵运算一次算一整层。2.2 Shape 和广播机制大模型里最容易翻车的地方Shape 这个概念刚开始学 NumPy 的时候觉得很简单不就是数组的维度吗但到了 Transformer 里shape 变换能把你绕到怀疑人生。我见过太多新手在跑模型时报错RuntimeError: mat1 and mat2 shapes cannot be multiplied根源就是没搞清楚 shape 怎么对齐。NumPy 的广播机制是这样的当两个数组做运算时如果 shape 不完全一致NumPy 会尝试从右往左逐维比较维度相等或其中一个为 1 时才能广播否则报错。举个例子一个 shape 为(3, 4)的矩阵和一个 shape 为(4,)的向量相加向量会被广播成(3, 4)每一行都加上这个向量。这个机制在 Transformer 里到处都是比如给每个 token 的 embedding 加上位置编码就是用一个(seq_len, d_model)的矩阵加上一个(seq_len, d_model)的位置编码矩阵shape 完全一致直接相加。但到了注意力机制里事情就复杂了。Query、Key、Value 三个矩阵的 shape 变换加上多头注意力的拆分和合并稍不注意就会搞错维度。我的经验是每次写代码之前先在纸上把每一步的 shape 写出来标注清楚哪个维度是 batch size哪个是序列长度哪个是特征维度。这个习惯帮我省了无数调试时间。2.3 矩阵运算从np.dot到np.matmul的坑NumPy 里做矩阵乘法有好几个函数np.dot、np.matmul、运算符还有np.multiply做逐元素乘法。新手很容易搞混。np.dot对于二维数组就是矩阵乘法但对于高维数组它的行为是“对最后两个维度做矩阵乘法前面的维度做外积”非常反直觉。np.matmul和的行为更一致它把数组当成一批矩阵对最后两个维度做矩阵乘法前面的维度是 batch 维度。所以在写 Transformer 代码时我强烈建议统一用或np.matmul避免np.dot带来的意外。还有一个常见错误是混淆了矩阵乘法和逐元素乘法。比如在计算注意力分数时Q K.T是矩阵乘法得到的是(seq_len, seq_len)的分数矩阵而如果你不小心写成了Q * K.T那就是逐元素乘法shape 对不上直接报错就算对上了结果也完全不对。这种错误在调试时很难发现因为代码不报错只是结果很差。我的做法是在关键步骤后面打印 shape确认每一步都符合预期。3. 从 NumPy 到 Tensor数据表示的统一视角3.1 Tensor 到底是什么和 ndarray 有什么区别PyTorch 的 Tensor 和 NumPy 的 ndarray 在概念上几乎一样都是多维数组。区别在于 Tensor 可以跑在 GPU 上支持自动求导并且有专门的深度学习算子优化。你可以把 Tensor 理解成“加强版的 ndarray”。事实上你可以用torch.from_numpy()把一个 ndarray 直接转成 Tensor内存是共享的改一个另一个也会变。反过来用.numpy()可以把 Tensor 转回 ndarray但前提是它不在 GPU 上也不需要梯度。我刚开始学的时候喜欢在 NumPy 里把数据预处理完再转成 Tensor 喂给模型。后来发现其实很多操作直接在 Tensor 上做更方便尤其是涉及到 GPU 加速的时候。比如归一化、切片、拼接Tensor 的 API 和 NumPy 几乎一一对应迁移成本很低。所以如果你 NumPy 用得熟上手 PyTorch 会非常快。3.2 Shape 在 Transformer 里的具体含义在 Transformer 里最常见的输入 shape 是(batch_size, seq_len, d_model)。这三个维度分别代表一次处理多少条样本每条样本有多少个 token每个 token 用多少维的向量表示。比如 batch_size32seq_len128d_model512意思就是一次喂给模型 32 句话每句话 128 个 token每个 token 是一个 512 维的向量。这个 shape 会随着网络层的变化而变化。经过多头注意力后d_model 会被拆成 num_heads 个头每个头的维度是 d_model // num_heads。比如 d_model512num_heads8每个头就是 64 维。这时候 shape 会变成(batch_size, num_heads, seq_len, head_dim)。计算完注意力后再合并回去变回(batch_size, seq_len, d_model)。这个拆分和合并的过程是 Transformer 代码里最容易写错的地方之一。我建议你手动画一遍这个 shape 变换的流程图比看十遍代码都管用。3.3 广播机制在注意力计算中的应用注意力机制的核心公式是Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) V。这里面每一步都涉及 shape 变换和广播。Q的 shape 是(batch_size, num_heads, seq_len, head_dim)K^T的 shape 是(batch_size, num_heads, head_dim, seq_len)两者做矩阵乘法得到(batch_size, num_heads, seq_len, seq_len)。然后除以sqrt(d_k)这里d_k是一个标量广播到所有元素。接着做 softmax在最后一个维度上归一化。最后和V做矩阵乘法V的 shape 是(batch_size, num_heads, seq_len, head_dim)得到(batch_size, num_heads, seq_len, head_dim)。如果你用 NumPy 手动实现一遍这个计算会对每一步的 shape 有非常深刻的理解。我当初就是用一个 2x2 的小矩阵手动算了一遍注意力才真正搞明白 Q、K、V 到底在干什么。那个“key 我是谁、query 我在找什么、value 我能提供什么”的比喻虽然有点粗糙但确实帮助记忆。4. Transformer 架构拆解从论文到可运行的代码4.1 自注意力机制Q、K、V 到底在算什么自注意力机制的本质是对于序列中的每一个 token它需要看看其他所有 token决定从谁那里获取信息。Query 代表当前 token 在“询问”什么Key 代表每个 token 能“回答”什么Value 代表每个 token 实际“提供”的信息。计算过程是用 Query 和所有 Key 做点积得到相关性分数softmax 归一化后作为权重对 Value 加权求和。用 NumPy 实现的话假设输入X的 shape 是(seq_len, d_model)三个权重矩阵W_q、W_k、W_v的 shape 都是(d_model, d_k)。那么Q X W_qK X W_kV X W_vshape 都是(seq_len, d_k)。然后scores Q K.T / sqrt(d_k)shape 是(seq_len, seq_len)。attention_weights softmax(scores)最后output attention_weights Vshape 是(seq_len, d_k)。这就是最基础的单头注意力。多头注意力就是把这个过程并行做多次每次用不同的W_q、W_k、W_v然后把结果拼接起来再过一个线性层。这样做的好处是不同的头可以关注不同的模式比如有的头关注语法关系有的头关注语义相似度。4.2 位置编码为什么需要它怎么实现Transformer 本身没有循环结构也没有卷积结构它对序列的顺序是无感知的。如果你把输入序列打乱自注意力的输出只会跟着打乱不会意识到顺序变了。所以需要位置编码给每个位置一个独特的向量加到 token 的 embedding 上。原始论文用的是正弦余弦位置编码公式是PE(pos, 2i) sin(pos / 10000^(2i/d_model))PE(pos, 2i1) cos(pos / 10000^(2i/d_model))。这个设计很巧妙不同位置的编码可以通过线性变换相互表示模型能学到相对位置关系。用 NumPy 实现的话就是创建一个(seq_len, d_model)的矩阵然后按公式填充。我建议你手动实现一遍感受一下不同维度的频率变化。现在很多模型改用可学习的位置编码就是直接用一个(max_seq_len, d_model)的矩阵让模型自己学。效果差不多但实现更简单。不过理解正弦编码的原理对理解位置信息的本质很有帮助。4.3 残差连接和层归一化让深层网络能训练残差连接就是output x sublayer(x)把输入直接加到输出上。这个操作看起来简单但它是深层网络能训练的关键。没有残差连接梯度在反向传播时会逐层衰减几十层之后基本就消失了。有了残差连接梯度可以走“捷径”直接回传训练起来稳定很多。层归一化是对每个样本的最后一维做归一化均值为 0方差为 1然后再缩放和平移。和 BatchNorm 不同LayerNorm 不依赖 batch 维度所以对 batch size 不敏感适合变长序列。在 Transformer 里通常是LayerNorm(x sublayer(x))先残差再归一化。也有先归一化再残差的变体叫 Pre-LN训练更稳定现在很多模型都用这种。用 NumPy 实现 LayerNorm 很简单mean x.mean(axis-1, keepdimsTrue)std x.std(axis-1, keepdimsTrue)output (x - mean) / (std eps) * gamma beta。其中gamma和beta是可学习的参数。注意eps要加在标准差上防止除零。4.4 前馈网络Transformer 里的“思考”层每个 Transformer 层里除了注意力还有一个前馈网络。它就是一个两层的全连接网络中间有 ReLU 激活FFN(x) max(0, x W1 b1) W2 b2。通常W1会把维度扩大 4 倍W2再缩回去。比如 d_model512中间层就是 2048。这个前馈网络的作用可以理解为对注意力收集到的信息进行“加工”和“存储”。注意力负责从序列中提取信息前馈网络负责对这些信息做非线性变换。有研究表明前馈网络里存储了大量的知识相当于模型的一个“记忆库”。用 NumPy 实现就是两个矩阵乘法加一个 ReLU。ReLU 就是np.maximum(0, x)非常简单。但要注意W1和W2的 shape 要匹配W1是(d_model, d_ff)W2是(d_ff, d_model)。5. 动手跑一个迷你 LLM从 NumPy 到 PyTorch5.1 环境准备和依赖安装先说环境。Python 版本建议 3.9 以上太老的版本有些库不支持。NumPy 安装很简单pip install numpy就行。如果你遇到ModuleNotFoundError: No module named numpy要么是没装要么是装到了别的 Python 环境里。我建议用虚拟环境python -m venv llm-env然后激活再装依赖。这样不会污染系统环境也方便管理版本。PyTorch 的安装稍微麻烦一点因为要选对 CUDA 版本。如果你没有 GPU直接装 CPU 版就行pip install torch torchvision torchaudio。有 GPU 的话去 PyTorch 官网查对应的安装命令。我踩过的坑是NumPy 版本和 PyTorch 版本不匹配导致torch.from_numpy()报错。解决办法是升级 NumPy 到最新版或者降级 PyTorch 到兼容版本。一般来说PyTorch 1.10 以上配 NumPy 1.21 以上都没问题。5.2 用 NumPy 实现一个单头注意力先不急着上 PyTorch用 NumPy 手动实现一个单头注意力能帮你彻底理解计算过程。假设输入X的 shape 是(seq_len, d_model)我们初始化三个权重矩阵import numpy as np np.random.seed(42) seq_len 4 d_model 8 d_k 4 X np.random.randn(seq_len, d_model) W_q np.random.randn(d_model, d_k) W_k np.random.randn(d_model, d_k) W_v np.random.randn(d_model, d_k) Q X W_q K X W_k V X W_v scores Q K.T / np.sqrt(d_k) attention_weights np.exp(scores) / np.exp(scores).sum(axis-1, keepdimsTrue) output attention_weights V print(Q shape:, Q.shape) print(K shape:, K.shape) print(V shape:, V.shape) print(scores shape:, scores.shape) print(attention_weights shape:, attention_weights.shape) print(output shape:, output.shape)跑一遍看看每个变量的 shape再手动算一下scores的第一个元素验证一下是不是Q[0]和K[0]的点积除以sqrt(d_k)。这个过程能帮你把注意力的计算逻辑刻在脑子里。5.3 用 PyTorch 搭建一个完整的 Transformer 层理解了 NumPy 版本后用 PyTorch 实现就简单多了因为很多操作都有现成的 API。下面是一个完整的 Transformer 层import torch import torch.nn as nn class TransformerLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout0.1): super().__init__() self.attention nn.MultiheadAttention(d_model, num_heads, dropoutdropout) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.ffn nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model) ) self.dropout nn.Dropout(dropout) def forward(self, x): attn_output, _ self.attention(x, x, x) x self.norm1(x self.dropout(attn_output)) ffn_output self.ffn(x) x self.norm2(x self.dropout(ffn_output)) return x注意nn.MultiheadAttention的输入默认是(seq_len, batch_size, d_model)和 NumPy 版本的(batch_size, seq_len, d_model)不一样。如果你习惯后者可以在输入前用transpose转一下。这个 shape 顺序的差异是很多人从 NumPy 转到 PyTorch 时第一个踩的坑。5.4 训练一个小型语言模型有了 Transformer 层就可以堆一个几层的小模型在字符级语言建模任务上训练。数据集可以用莎士比亚的文本或者任何你喜欢的文本。任务很简单给模型一个字符序列让它预测下一个字符。损失函数用交叉熵优化器用 Adam。训练循环大概是这样的把文本转成字符索引切成长度为 seq_len 的片段输入模型得到每个位置的输出和真实的下一个字符做交叉熵。反向传播更新参数。跑几千步之后模型就能生成看起来有点意思的文本了。虽然规模很小但整个流程和大模型训练是一样的只是数据量和参数量不同。我建议你在这个小模型上多调调参数比如改改层数、头数、学习率观察损失曲线的变化。这种“手感”对后面理解大模型的训练动态很有帮助。6. 常见问题与排查技巧实录6.1 Shape 不匹配最常见的报错及解决思路Shape 不匹配是深度学习里最高频的报错没有之一。典型错误信息是RuntimeError: mat1 and mat2 shapes cannot be multiplied或者ValueError: operands could not be broadcast together。排查思路很简单在报错的那一行前面把涉及到的所有张量的 shape 打印出来逐个对照。比如矩阵乘法A B要求A的最后一维等于B的倒数第二维。如果A是(32, 128, 512)B是(512, 768)那没问题结果是(32, 128, 768)。但如果B是(256, 768)就会报错。这时候要么改B的 shape要么在A和B之间加一个线性层做维度变换。广播错误通常是两个数组的 shape 从右往左比较时某一维既不相等也不为 1。比如(3, 4)和(3, 5)相加就会报错。解决办法是reshape或者unsqueeze补维度。6.2 梯度消失和梯度爆炸现象、原因和应对梯度消失的表现是模型训练一段时间后损失不再下降参数几乎不变。原因是反向传播时梯度逐层相乘如果每层的梯度都小于 1几十层之后就会趋近于 0。梯度爆炸则相反损失突然变成 NaN原因是梯度乘积太大数值溢出。应对方法有几个一是用残差连接让梯度有捷径可走二是用 LayerNorm把每层的输出归一化稳定数值范围三是用梯度裁剪把梯度的范数限制在一个阈值内四是选合适的初始化方法比如 Xavier 或 Kaiming 初始化。我在训练小模型时通常加上梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)效果很明显。6.3 显存不够用从 batch size 到混合精度跑大模型最头疼的就是显存不够。报错通常是CUDA out of memory。解决办法按优先级排第一减小 batch size这是最直接的第二用梯度累积把多个小 batch 的梯度累加起来再更新等效于大 batch第三用混合精度训练torch.cuda.amp把部分计算转成 float16显存占用能降一半左右第四用梯度检查点牺牲计算时间换显存第五如果还不行就只能换更大的显卡或者用模型并行。我自己的经验是先从小 batch size 开始跑通再逐步增大找到显存的上限。同时用torch.cuda.memory_summary()看看显存都花在哪了有时候是中间激活值占了大头这时候梯度检查点就很管用。6.4 常见问题速查表问题现象可能原因排查方法解决方案ModuleNotFoundError: No module named numpy未安装或环境不对pip list查看pip install numpy或激活正确环境mat1 and mat2 shapes cannot be multiplied矩阵维度不匹配打印两个矩阵的 shape调整维度或加线性层损失变成 NaN梯度爆炸或学习率太大打印梯度范数梯度裁剪、降低学习率显存不足batch size 太大或模型太大torch.cuda.memory_summary()减小 batch、混合精度、梯度检查点模型不收敛学习率不当或初始化不好观察损失曲线调学习率、换初始化方法NumPy 版本不匹配版本冲突pip show numpy升级或降级到兼容版本7. 从迷你模型到 LLM下一步可以怎么走跑通迷你模型之后你对 Transformer 的整个数据流应该已经有了直观感受。接下来如果想继续深入有几个方向可以选。一是读开源大模型的代码比如 LLaMA、GPT-NeoX看看工业级实现和教学实现有什么区别比如 KV Cache、旋转位置编码、分组查询注意力这些优化技巧。二是用 Hugging Face 的 Transformers 库加载预训练模型做推理和微调感受一下真正的 LLM 能力。三是研究 LLM 的应用层比如 RAG、Agent、知识库这些方向对算力要求低但工程复杂度高适合开发者切入。我个人觉得从 NumPy 到 Transformer 再到 LLM这条路径最大的价值不是让你成为算法专家而是让你对“大模型到底在干什么”有一个祛魅的理解。你知道它不是什么魔法就是一堆矩阵运算和梯度下降只是规模大到了产生质变的程度。有了这个认知再去用 LLM 做应用、做产品心里会踏实很多遇到问题也知道从哪下手排查。最后分享一个小技巧如果你在调试 Transformer 代码时卡住了不妨把序列长度设为 2模型维度设为 4头数设为 1然后用 NumPy 手动算一遍前向传播再和 PyTorch 的输出对比。如果两者一致说明你的理解是对的如果不一致逐层对比很快就能找到问题。这个“最小复现”的思路是我调试所有深度学习代码的万能钥匙。