从NumPy到Transformer:程序员学LLM必须补的数学工具与Shape思维

发布时间:2026/11/3 23:39:13
从NumPy到Transformer:程序员学LLM必须补的数学工具与Shape思维

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, dtype=np.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(f"Python list: {t1 - t0:.3f}s") print(f"NumPy: {t3 - t2:.3f}s")

在我这台普通笔记本上,list 版本大概要 1.5 秒,NumPy 版本 0.02 秒左右,差了将近两个数量级。原因就是 NumPy 把循环下沉到了 C 语言层面,还利用了 SIMD 指令并行处理。这就是"向量化思维"的起点——能用数组整体运算,就别写 for 循环。

2.2 安装 NumPy 的正确姿势与版本坑

numpy安装看着简单,坑却不少。最稳的方式是用 pip:

pip install numpy

但如果你用的是 conda 环境,建议:

conda install numpy

因为 conda 会帮你处理底层的 BLAS/LAPACK 数学库依赖,矩阵运算性能更好。纯 pip 装的话,有时候会拉到一个没链接优化数学库的版本,做矩阵乘法会慢一截。

关于numpy版本不匹配,这是新手最容易踩的坑。典型场景是:你装了 PyTorch,它依赖某个 NumPy 版本,你又手动升级了 NumPy,结果两者 ABI 不兼容,一 import 就报错。我的经验是:

  • 先装框架(PyTorch/TensorFlow),让它们自己决定 NumPy 版本,别手动去动。
  • 如果非要升级,用pip install "numpy<2.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_grad=True);三是 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 手写一遍单头注意力。下面这段代码是我自己学习时写的,每一步都标了 shape:

import numpy as np def softmax(x, axis=-1): x = x - np.max(x, axis=axis, keepdims=True) # 数值稳定 exp = np.exp(x) return exp / np.sum(exp, axis=axis, keepdims=True) 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每行加起来是 1;out的 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_heads

transformer架构模型参数计算里,注意力部分的参数量主要来自四个投影矩阵 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_model=64, num_heads=4, ff_dim=256): super().__init__() self.attn = nn.MultiheadAttention(d_model, num_heads, batch_first=True) 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 numpy
numpy版本不匹配框架与 NumPy ABI 冲突先装框架,别手动升 NumPy
矩阵乘法报 shape 错维度不满足乘法规则打印两边 shape,检查对齐
广播结果不对维度恰好能广播但语义错运算后立即校验 shape
GPU 上运算报错张量还在 CPU检查.to(device)是否一致

6.2 几个我踩过的坑

第一个坑是盲目升级 NumPy。有次为了用某个新函数把 NumPy 升到最新,结果 PyTorch 直接 import 失败,回退版本才恢复。教训是:在框架环境里,NumPy 版本交给框架管。

第二个坑是忽略 dtype。NumPy 默认 float64,PyTorch 默认 float32。混用的时候要么报错要么悄悄降精度。我现在的习惯是显式指定dtype=np.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 的角度读懂一个模型,你就已经跨过了从"调包"到"理解"的那道坎。