
1. 从一行 NumPy 说起为什么程序员学 LLM 要回头补数学工具很多人第一次接触大模型脑子里想的都是 Transformer、注意力机制、Token、微调这些词恨不得当天就把一个开源模型跑起来。但真到动手的时候卡住你的往往不是模型本身而是最底层的一行代码——import numpy as np报了个ModuleNotFoundError: No module named numpy或者装上了却提示numpy版本不匹配再或者你压根没搞明白shape到底在描述什么。我自己就是从这条路走过来的所以这篇笔记不打算一上来就讲注意力公式而是老老实实从 NumPy 这个地基讲起一路铺到 Transformer 和大模型的整体认知。这篇内容适合三类人一是刚学 Python、想往 AI 方向走的初学者二是会写业务代码、但对大模型内部机制一知半解的工程师三是想系统梳理一遍从数组到模型这条链路、把零散知识串起来的人。核心关键词会贯穿始终NumPy、LLM、Transformer、Tensor、Shape。我会把每个环节为什么这么做讲清楚而不是只丢给你一段能跑的代码。先说清楚一个定位NumPy 不是过时的老古董恰恰相反它是整个 Python 科学计算和数据科学生态的底座。你后面要用的 PyTorch、TensorFlow、JAX它们的张量Tensor概念和 NumPy 的 ndarray 是一脉相承的。理解了 NumPy 的数组、广播、形状变换你再看 Transformer 里的张量运算会有一种原来就是换个名字的顿悟感。这也是我坚持从 NumPy 讲起的原因——跳过它直接学 LLM等于盖楼不打地基。2. NumPy 到底解决了什么问题从 list 到 ndarray 的跨越2.1 NumPy 和 list 比快在哪新手最常问的一个问题就是Python 自带的 list 不也能存一堆数吗为什么还要 NumPy这个问题问得特别好答案藏在内存布局和运算方式两个层面。list 是一个指针数组里面存的不是数字本身而是指向各个 Python 对象的指针。每个 Python 的 int 或 float 都是一个完整的对象带着类型信息、引用计数等一堆额外开销。你存一百万个浮点数list 里就有一百万个独立的 Python 对象内存占用大、访问还绕。NumPy 的 ndarray 则是一块连续的、同类型的内存。它只存数据本身类型统一比如全是 float32外加一个描述形状和步长的头部信息。这样一来CPU 可以一次性把连续内存读进缓存做向量化运算速度差距往往是几十倍甚至上百倍。我实测过一个简单的例子对一千万个数做逐元素平方再求和import numpy as np import time n 10_000_000 py_list list(range(n)) np_arr np.arange(n, dtypenp.float64) # 纯 Python t0 time.time() s1 sum(x * x for x in py_list) t1 time.time() # NumPy t2 time.time() s2 np.sum(np_arr * np_arr) t3 time.time() print(fPython list: {t1 - t0:.3f}s) print(fNumPy: {t3 - t2:.3f}s)在我这台普通笔记本上list 版本大概要 1.5 秒NumPy 版本 0.02 秒左右差了将近两个数量级。原因就是 NumPy 把循环下沉到了 C 语言层面还利用了 SIMD 指令并行处理。这就是向量化思维的起点——能用数组整体运算就别写 for 循环。2.2 安装 NumPy 的正确姿势与版本坑numpy安装看着简单坑却不少。最稳的方式是用 pippip install numpy但如果你用的是 conda 环境建议conda install numpy因为 conda 会帮你处理底层的 BLAS/LAPACK 数学库依赖矩阵运算性能更好。纯 pip 装的话有时候会拉到一个没链接优化数学库的版本做矩阵乘法会慢一截。关于numpy版本不匹配这是新手最容易踩的坑。典型场景是你装了 PyTorch它依赖某个 NumPy 版本你又手动升级了 NumPy结果两者 ABI 不兼容一 import 就报错。我的经验是先装框架PyTorch/TensorFlow让它们自己决定 NumPy 版本别手动去动。如果非要升级用pip install numpy2.0这种带约束的方式别裸升。遇到ModuleNotFoundError: no module named numpy先确认你当前用的 Python 解释器和 pip 是不是同一个环境虚拟环境里最容易出这个问题。提示用python -c import numpy; print(numpy.__version__)快速确认当前环境实际生效的 NumPy 版本比看 pip list 更准。2.3 Shape 是理解一切张量运算的钥匙Shape这个词是贯穿 NumPy 和 Transformer 的核心概念。你可以把 shape 理解成这个数组在每个维度上有多少个元素。a np.array([1, 2, 3]) # shape (3,) 一维 b np.array([[1, 2], [3, 4]]) # shape (2, 2) 二维 c np.zeros((2, 3, 4)) # shape (2, 3, 4) 三维为什么 shape 这么重要因为几乎所有张量运算的报错本质都是 shape 对不上。矩阵乘法要求前一个的最后一维等于后一个的倒数第二维广播broadcasting要求维度要么相等要么其中一个是 1。你在 Transformer 里看到的一堆reshape、transpose、unsqueeze全都是在跟 shape 打交道。我个人的习惯是写任何张量代码前先在纸上或注释里把每一步的 shape 标出来。这个习惯帮我省下了无数调试时间。比如一句话经过 tokenizer 后是(batch, seq_len)过 embedding 变成(batch, seq_len, d_model)进多头注意力又要拆成(batch, num_heads, seq_len, head_dim)。shape 想清楚了代码基本就对了。3. 从 NumPy 到 Tensor概念是怎么平滑过渡的3.1 Tensor 不是新东西是 NumPy 的进化版很多人被Tensor这个词吓到觉得是什么高深概念。其实你可以直接理解成Tensor 就是能放到 GPU 上、能自动求导的 NumPy 数组。它的 shape、索引、切片、广播规则跟 ndarray 几乎一模一样。import torch # NumPy 风格 x torch.tensor([[1.0, 2.0], [3.0, 4.0]]) print(x.shape) # torch.Size([2, 2]) # 和 NumPy 互转 import numpy as np arr np.array([[1.0, 2.0], [3.0, 4.0]]) t torch.from_numpy(arr) # NumPy - Tensor back t.numpy() # Tensor - NumPy关键差异有三个一是 Tensor 可以搬到 GPU.to(cuda)二是 Tensor 支持自动微分requires_gradTrue三是 Tensor 有专门的tensor core加速在支持矩阵乘的硬件上能跑出极高吞吐。理解了这三点你就明白为什么深度学习框架不直接用 NumPy 了——NumPy 缺的正是求导和硬件加速这两块。3.2 广播机制让 shape 不同的数组也能运算广播是 NumPy 里最优雅也最容易搞混的机制。规则很简单从右往左对齐维度每个维度要么相等要么其中一个是 1要么其中一个不存在。a np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3) b np.array([10, 20, 30]) # shape (3,) print(a b) # [[11, 22, 33], # [14, 25, 36]]这里b被自动广播成了(2, 3)每一行都加上[10, 20, 30]。在 Transformer 里位置编码加到词嵌入上、注意力分数加 mask用的都是广播。掌握广播你就能少写很多显式的循环和复制。但广播也是 bug 温床。比如你本想按行归一化结果写成了按列shape 恰好能广播过去程序不报错但结果是错的。我的建议是运算后立刻检查 shape 是否符合预期别等 loss 不下降才回头找。3.3 矩阵运算从求逆到行列式numpy如何求解矩阵的逆和python行列式计算不使用numpy这类问题说明很多人卡在线性代数上。NumPy 提供了完整的线性代数模块import numpy as np A np.array([[4.0, 7.0], [2.0, 6.0]]) # 求逆 A_inv np.linalg.inv(A) # 行列式 det np.linalg.det(A) # 解线性方程组 Ax b b np.array([1.0, 2.0]) x np.linalg.solve(A, b)这里有个实操心得能用solve就别用inv。因为求逆再相乘在数值上不稳定计算量也更大。np.linalg.solve内部用的是更稳定的分解方法。这个原则在深度学习里同样适用——很多看似要求逆的操作实际都有更稳的等价写法。至于不使用 NumPy 算行列式纯 Python 可以用递归展开或高斯消元实现但性能和数值稳定性都远不如 NumPy。除非是教学目的否则没必要自己造轮子。4. Transformer 拆解从 Shape 视角看懂注意力机制4.1 一句话讲清 Transformer 在干什么transformer模型详解的文章满天飞但很多一上来就是公式。我用一句话概括Transformer 就是让序列里的每个位置去看其他所有位置然后根据相关性加权汇总信息。这个看的过程就是注意力Attention。而llm的token三个点key我是谁、query我在找什么、value我能提供什么这个热词把 QKV 讲得特别形象。我顺着这个说法展开Query我在找什么当前位置发出的查询请求。Key我是谁每个位置对外展示的身份标签。Value我能提供什么每个位置实际携带的内容信息。注意力做的事就是用我的 Query 去和所有位置的 Key 做匹配算出相似度再用相似度对所有 Value 加权求和。匹配度高说明这个位置对我重要它的 Value 就多拿一点。4.2 用 NumPy 手写一遍注意力彻底搞懂 shape光看公式容易飘我建议你用 NumPy 手写一遍单头注意力。下面这段代码是我自己学习时写的每一步都标了 shapeimport numpy as np def softmax(x, axis-1): x x - np.max(x, axisaxis, keepdimsTrue) # 数值稳定 exp np.exp(x) return exp / np.sum(exp, axisaxis, keepdimsTrue) def self_attention(X, Wq, Wk, Wv): # X: (seq_len, d_model) Q X Wq # (seq_len, d_k) K X Wk # (seq_len, d_k) V X Wv # (seq_len, d_v) scores Q K.T # (seq_len, seq_len) scores scores / np.sqrt(Q.shape[-1]) # 缩放 weights softmax(scores, axis-1) # (seq_len, seq_len) out weights V # (seq_len, d_v) return out, weights seq_len, d_model, d_k 4, 8, 8 X np.random.randn(seq_len, d_model) Wq np.random.randn(d_model, d_k) Wk np.random.randn(d_model, d_k) Wv np.random.randn(d_model, d_k) out, weights self_attention(X, Wq, Wk, Wv) print(输出 shape:, out.shape) # (4, 8) print(注意力权重 shape:, weights.shape) # (4, 4)跑一遍你就明白了scores是(seq_len, seq_len)表示每个位置对其他位置的关注程度weights每行加起来是 1out的 shape 和输入一致。这就是 Transformer 的核心剩下的多头、残差、LayerNorm 都是在这个骨架上加料。4.3 为什么要缩放、为什么要多头先说缩放。scores / sqrt(d_k)这一步不是可有可无的。当d_k很大时Q 和 K 的点积方差会随维度增大导致 softmax 的输入数值很大输出接近 one-hot梯度几乎消失。除以sqrt(d_k)把方差拉回 1 附近训练才稳定。这是为什么层面的知识比记住公式重要得多。再说多头。单头注意力只能学到一种关注模式多头就是并行跑多组 QKV每组关注不同的关系——有的头关注语法、有的关注指代、有的关注位置。最后把多头的输出拼接再投影回d_model。从 shape 看# 输入 (batch, seq_len, d_model) # 拆成 (batch, num_heads, seq_len, head_dim) # 其中 head_dim d_model // num_headstransformer架构模型参数计算里注意力部分的参数量主要来自四个投影矩阵 Wq、Wk、Wv、Wo每个都是d_model × d_model所以是4 × d_model²。这个数字在估算模型大小时很有用。4.4 从 Transformer 到 LLM架构没变规模变了大模型llm和transformer的关系一句话主流 LLM 就是解码器版 Transformer堆很多层、喂很多数据训出来的。GPT 系列用的是 decoder-only 结构只保留带因果 mask 的自注意力每个位置只能看前面的不能看后面加上前馈网络和残差。transformer架构本身没变变的是规模层数从 6 层涨到几十上百层d_model从 512 涨到几千上万参数量从几千万涨到千亿级。vision transformer则是把图像切成 patch 当 token套用同一套架构做图像分类swin transformer进一步引入层级和窗口注意力来降低计算量。同一套思想换个输入形式就能跨模态。5. 实操路线一个程序员该怎么系统学下去5.1 分阶段的学习路径我把自己的学习路线整理成四个阶段供你参考阶段目标关键内容检验标准第一阶段打牢 NumPy数组、shape、广播、矩阵运算能手写 softmax、矩阵乘法第二阶段理解 TransformerQKV、多头、位置编码、残差能用 NumPy 手写单头注意力第三阶段上手框架PyTorch 张量、自动求导、nn.Module能跑通一个小 Transformer第四阶段接触 LLMTokenizer、加载预训练模型、推理能本地跑通一个开源小模型这个顺序不能乱。我见过太多人跳过前两步直接冲第四步结果连报错都看不懂。基础不牢调参调到怀疑人生。5.2 手写一个最小 Transformer 块在 NumPy 版注意力之后我建议用 PyTorch 写一个最小的 Transformer block把前面学的 shape 知识用起来import torch import torch.nn as nn class MiniTransformerBlock(nn.Module): def __init__(self, d_model64, num_heads4, ff_dim256): super().__init__() self.attn nn.MultiheadAttention(d_model, num_heads, batch_firstTrue) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.ff nn.Sequential( nn.Linear(d_model, ff_dim), nn.ReLU(), nn.Linear(ff_dim, d_model), ) def forward(self, x): # x: (batch, seq_len, d_model) attn_out, _ self.attn(x, x, x) x self.norm1(x attn_out) # 残差 LayerNorm x self.norm2(x self.ff(x)) # 残差 LayerNorm return x block MiniTransformerBlock() x torch.randn(2, 10, 64) # (batch, seq_len, d_model) print(block(x).shape) # (2, 10, 64)跑通这段你就理解了 Transformer block 的输入输出 shape 不变这个关键特性——正因为不变才能一层层往上堆。这也是为什么 Transformer 能堆得那么深。5.3 关于 LLM 知识库和工具链的补充热词里出现了llm wiki知识库、llm wiki项目、karpathy llm wiki这些说明很多人想找一个系统整理 LLM 知识的入口。我的建议是与其收藏一堆链接不如自己动手建一个笔记库把每个概念用自己的话写一遍配上能跑的代码。能讲清楚才是真懂。工具链方面onnx部署llm模型、llm 网关、llm框架这些方向属于工程落地层面。等你把模型原理搞懂了再去看这些会轻松很多。至于open llm leaderboard这类公开榜单可以用来选型参考但别唯榜单论——榜单分数高不等于适合你的场景。6. 常见问题与避坑实录6.1 环境与版本类问题速查问题现象常见原因解决思路ModuleNotFoundError: no module named numpy解释器和 pip 不在同一环境用python -m pip install numpynumpy版本不匹配框架与 NumPy ABI 冲突先装框架别手动升 NumPy矩阵乘法报 shape 错维度不满足乘法规则打印两边 shape检查对齐广播结果不对维度恰好能广播但语义错运算后立即校验 shapeGPU 上运算报错张量还在 CPU检查.to(device)是否一致6.2 几个我踩过的坑第一个坑是盲目升级 NumPy。有次为了用某个新函数把 NumPy 升到最新结果 PyTorch 直接 import 失败回退版本才恢复。教训是在框架环境里NumPy 版本交给框架管。第二个坑是忽略 dtype。NumPy 默认 float64PyTorch 默认 float32。混用的时候要么报错要么悄悄降精度。我现在的习惯是显式指定dtypenp.float32和框架保持一致。第三个坑是softmax 不减去最大值。手写 softmax 时如果直接np.exp(x)遇到大数值会溢出成 inf。减去max是标准做法别省这一步。第四个坑是把 shape 记在脑子里。多维张量一多人脑根本记不住。我的做法是每写一步就print(x.shape)或者用注释标出来。看起来笨但省时间。6.3 关于学习节奏的一点个人体会学 LLM 最容易犯的错是贪多求快。今天看 Transformer明天想微调后天又想搞 Agent。我的建议是一次只啃一个点啃到能给别人讲明白为止。NumPy 的广播、注意力的 shape 变换、残差为什么能缓解梯度消失这些点一个个吃透比囫囵吞枣看十篇综述强得多。另外transformer手写这件事真的值得做一遍。不用写得多完整哪怕只是一个单头注意力加一个前馈层亲手敲一遍你对 shape 和矩阵运算的理解会完全不一样。我当初就是靠手写这一遍才真正把QKV 到底在算什么想清楚的。最后分享一个小技巧遇到看不懂的架构图别硬看直接去找对应的代码把每一行的 shape 标出来。图会骗人shape 不会。当你能从 shape 的角度读懂一个模型你就已经跨过了从调包到理解的那道坎。