news 2026/9/30 5:08:47

从NumPy到LLM:程序员如何用矩阵运算理解大语言模型核心原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从NumPy到LLM:程序员如何用矩阵运算理解大语言模型核心原理

1. 从一行 NumPy 说起:为什么程序员该懂点 LLM

1.1 一个真实的学习起点

我最早接触 NumPy 的时候,纯粹是为了处理一批传感器采集的数据。那时候的想法很简单:Python 的 list 用着挺顺手,为什么还要学一个新库?直到我用 list 做了一次 1000×1000 的矩阵乘法,跑了将近 8 秒,换成 NumPy 之后直接降到 0.02 秒。这个差距让我第一次意识到,底层数据结构的选择,直接决定了上层能做什么。

后来我开始接触大语言模型(LLM),发现一个很有意思的现象:几乎所有 LLM 的底层运算,归根结底都是 NumPy 那套东西的延伸——张量(Tensor)运算、矩阵乘法、广播机制、维度变换。Transformer 架构看起来很复杂,但拆开来看,每一层都是矩阵运算的堆叠。你如果理解了 NumPy 里的shape、broadcast、einsum,再看 Transformer 的代码,会发现很多“魔法”其实没那么神秘。

这篇笔记就是记录我从一行 NumPy 代码出发,一步步走到理解 LLM 核心原理的过程。适合谁看?如果你是有一定 Python 基础、想搞明白大模型到底怎么运转的程序员,或者你已经在用 LLM 的 API 但想往下挖一层,那这些内容应该对你有用。我不会堆砌公式,而是尽量用代码和类比把关键概念讲清楚。

1.2 从 NumPy 到 Tensor 的自然延伸

NumPy 的ndarray和深度学习框架里的Tensor,本质上描述的是同一件事:多维数组加上一套运算规则。区别在于,Tensor 多了两个能力——自动求导和硬件加速(GPU/TPU)。但如果你把这两个能力暂时放一边,Tensor 的很多操作逻辑和 NumPy 几乎一模一样。

举个例子,你在 NumPy 里写a @ b做矩阵乘法,在 PyTorch 里写a @ b做的是同样的事。你在 NumPy 里用a.reshape(2, 3, 4)改变数组形状,在 PyTorch 里也是a.reshape(2, 3, 4)。甚至广播机制(broadcasting)的规则都高度一致。这意味着,你花时间学 NumPy 的每一分钟,在学 LLM 的时候都会变成已有的知识储备。

我见过不少新手一上来就想跑 Transformer 模型,结果卡在shape不匹配的报错上,一调就是半天。其实这类问题的根源往往不在 Transformer 本身,而在对多维数组运算的理解不够扎实。所以我的建议是:别急着看 Transformer 的论文,先把 NumPy 里的矩阵运算、广播、轴变换这几个概念吃透,后面会省很多时间。

2. NumPy 核心操作:LLM 的地基怎么打

2.1 矩阵乘法:从手算到 einsum

矩阵乘法是 LLM 里出现频率最高的运算。Transformer 里的注意力机制、前馈网络、投影层,全都是矩阵乘法。NumPy 提供了几种写法:

import numpy as np # 方式一:np.dot a = np.random.randn(4, 8) b = np.random.randn(8, 16) c = np.dot(a, b) # shape: (4, 16) # 方式二:@ 运算符(推荐) c = a @ b # 同上 # 方式三:np.matmul(支持批量) a_batch = np.random.randn(2, 4, 8) b_batch = np.random.randn(2, 8, 16) c_batch = np.matmul(a_batch, b_batch) # shape: (2, 4, 16)

np.dot和@在二维情况下等价,但到了三维及以上,行为就不一样了。np.dot做的是张量缩并,而np.matmul(也就是@)做的是批量矩阵乘法。在 LLM 的代码里,你几乎总是需要批量矩阵乘法,因为输入是一个 batch 的数据。所以养成用@的习惯,能避免很多维度上的困惑。

再来说np.einsum,这个函数看起来吓人,但用熟了之后非常优雅。注意力机制里的Q @ K^T可以用 einsum 写成:

# Q: (batch, seq_len, d_k) # K: (batch, seq_len, d_k) # 计算注意力分数 scores = np.einsum('bqd,bkd->bqk', Q, K)

这行代码的意思是:对 Q 和 K 的最后一个维度做点积,保留 batch 和序列长度维度。用@写就是Q @ K.transpose(0, 2, 1),效果一样,但 einsum 的写法更直观地表达了“我在做什么运算”。当然,einsum 在性能上不一定总是最优,实际工程中还是用@居多,但理解 einsum 对读懂论文里的公式很有帮助。

2.2 广播机制:让 shape 不再打架

广播(broadcasting)是 NumPy 里最容易被低估的特性。它的规则很简单:从右往左对齐维度,要么相等,要么其中一个是 1,要么其中一个不存在。但就是这个简单规则,在 LLM 里到处都在用。

比如在 Transformer 的位置编码里,你需要把一个(seq_len, d_model)的编码矩阵加到(batch, seq_len, d_model)的输入上。NumPy 会自动把编码矩阵广播到 batch 维度:

# positional_encoding: (seq_len, d_model) # x: (batch, seq_len, d_model) x = x + positional_encoding # 自动广播

再比如层归一化(LayerNorm)里,你需要对每个样本的最后一维做归一化,然后乘以一个可学习的缩放参数gamma(shape 为(d_model,))。广播机制让(batch, seq_len, d_model)和(d_model,)能直接相乘,不需要手动扩展维度。

我踩过的一个坑是:广播虽然方便,但容易掩盖维度错误。比如你本意是想让两个(batch, 1)的向量做外积得到(batch, batch),结果因为广播规则,得到了一个(batch, batch)的矩阵,但语义完全不对。所以我的经验是:在关键运算前,养成打印 shape 的习惯。一行print(x.shape)能帮你省下大量调试时间。

2.3 Shape 变换:view、reshape、transpose 的区别

在 LLM 代码里,shape 变换无处不在。多头注意力机制就是一个典型的例子:你需要把(batch, seq_len, d_model)的输入拆成(batch, num_heads, seq_len, d_head),其中d_model = num_heads * d_head。

batch, seq_len, d_model = x.shape num_heads = 8 d_head = d_model // num_heads # 拆分多头 x = x.reshape(batch, seq_len, num_heads, d_head) x = x.transpose(0, 2, 1, 3) # (batch, num_heads, seq_len, d_head)

这里reshape和transpose配合使用。注意transpose之后,数组在内存里不再连续,如果后续要做view操作会报错。这时候需要先.contiguous()(PyTorch)或.copy()(NumPy)。这个细节在实际写代码时经常遇到,尤其是调试 shape 报错的时候。

还有一个容易混淆的点:reshape和view的区别。在 NumPy 里只有reshape,它会尽量返回视图,如果不行就复制。在 PyTorch 里,view要求内存连续,reshape更灵活。新手建议统一用reshape,除非你明确知道内存布局是连续的,并且追求极致性能。

3. Transformer 拆解:从 NumPy 视角看懂注意力机制

3.1 注意力的三个关键:Query、Key、Value

Transformer 的核心是自注意力机制(Self-Attention)。很多教程一上来就抛公式,但我觉得用一句话就能说清楚:

Query 是“我在找什么”,Key 是“我有什么”,Value 是“我能提供什么”。

具体来说,每个 token 会生成三个向量:Query(Q)、Key(K)、Value(V)。注意力分数的计算过程是:用我的 Query 去和所有 token 的 Key 做点积,得到一组权重,然后用这些权重对所有 token 的 Value 做加权求和。

用 NumPy 写出来就是:

# x: (batch, seq_len, d_model) # W_q, W_k, W_v: (d_model, d_k) Q = x @ W_q # (batch, seq_len, d_k) K = x @ W_k # (batch, seq_len, d_k) V = x @ W_v # (batch, seq_len, d_v) # 计算注意力分数 scores = Q @ K.transpose(0, 2, 1) # (batch, seq_len, seq_len) scores = scores / np.sqrt(d_k) # 缩放 # Softmax 归一化 weights = np.exp(scores - scores.max(axis=-1, keepdims=True)) weights = weights / weights.sum(axis=-1, keepdims=True) # 加权求和 output = weights @ V # (batch, seq_len, d_v)

这段代码就是自注意力的全部核心。看起来不复杂,对吧?但有几个细节值得展开。

为什么要除以sqrt(d_k)?因为当d_k很大时,Q 和 K 的点积会变得很大,导致 softmax 的梯度极小,训练不动。除以sqrt(d_k)相当于把方差拉回到 1 附近,让 softmax 的输出更平滑。这个技巧叫“缩放点积注意力”(Scaled Dot-Product Attention)。

Softmax 为什么要减去最大值?这是数值稳定性的考虑。exp函数在输入很大时会溢出,减去最大值后,最大的输入变成 0,exp(0)=1,不会溢出。这个技巧在 NumPy 里手动实现 softmax 时一定要加上。

3.2 多头注意力:并行的智慧

单头注意力只能捕捉一种关系模式。多头注意力(Multi-Head Attention)的思路是:用多组不同的 Q、K、V 投影,让模型同时关注不同的特征子空间。

实现上,就是把d_model拆成num_heads份,每份独立做注意力,最后拼接起来再投影一次。用 NumPy 写:

# 假设 d_model = 512, num_heads = 8, d_head = 64 # x: (batch, seq_len, 512) # 一次性计算所有头的 Q、K、V Q = x @ W_q # (batch, seq_len, 512) K = x @ W_k V = x @ W_v # 拆分成多头 Q = Q.reshape(batch, seq_len, num_heads, d_head).transpose(0, 2, 1, 3) K = K.reshape(batch, seq_len, num_heads, d_head).transpose(0, 2, 1, 3) V = V.reshape(batch, seq_len, num_heads, d_head).transpose(0, 2, 1, 3) # 每个头独立做注意力 scores = Q @ K.transpose(0, 1, 3, 2) # (batch, num_heads, seq_len, seq_len) weights = softmax(scores / np.sqrt(d_head)) output = weights @ V # (batch, num_heads, seq_len, d_head) # 拼接多头 output = output.transpose(0, 2, 1, 3).reshape(batch, seq_len, d_model) output = output @ W_o # 最终投影

这里的关键操作是transpose和reshape的配合。transpose(0, 2, 1, 3)把num_heads维度换到前面,让每个头的数据在内存里连续,方便后续的批量矩阵乘法。最后再换回来,用reshape把多头的结果拼回d_model。

我一开始看这段代码的时候,被 transpose 的参数绕晕了。后来我的方法是:在纸上画出每个维度的含义,然后一步步跟踪 shape 的变化。比如(batch, seq_len, num_heads, d_head)经过transpose(0, 2, 1, 3)变成(batch, num_heads, seq_len, d_head),这样就能清楚地知道每个维度代表什么。

3.3 位置编码:给模型一个“顺序感”

自注意力机制本身是位置无关的——把输入序列打乱,注意力的输出不变。但语言是有顺序的,所以需要位置编码(Positional Encoding)来注入位置信息。

原始 Transformer 用的是正弦位置编码:

def positional_encoding(seq_len, d_model): pos = np.arange(seq_len)[:, np.newaxis] # (seq_len, 1) i = np.arange(d_model)[np.newaxis, :] # (1, d_model) angle = pos / np.power(10000, (2 * (i // 2)) / d_model) encoding = np.zeros((seq_len, d_model)) encoding[:, 0::2] = np.sin(angle[:, 0::2]) encoding[:, 1::2] = np.cos(angle[:, 1::2]) return encoding

这段代码用到了 NumPy 的广播和切片赋值。pos是(seq_len, 1),i是(1, d_model),相除之后广播成(seq_len, d_model)。然后偶数维度用 sin,奇数维度用 cos。

现在主流的大模型(如 LLaMA、GPT 系列)更多用旋转位置编码(RoPE)或可学习的位置编码。但不管哪种,核心思路是一样的:给每个位置一个独特的向量表示,让模型能区分不同位置的 token。

4. 从 NumPy 到 LLM 框架:工具链的演进

4.1 为什么需要专门的框架

用 NumPy 手写一个 Transformer 的前向传播,对于理解原理非常有帮助。但如果你真的要用它来训练模型,很快就会遇到瓶颈:

  • 没有自动求导:你需要手动推导每个操作的梯度,工作量巨大且容易出错。
  • 没有 GPU 加速:NumPy 默认在 CPU 上运行,训练大模型的速度无法接受。
  • 没有优化器:SGD、Adam 等优化器需要自己实现。

所以实际工程中,我们会用 PyTorch、TensorFlow、JAX 等框架。这些框架提供了自动求导、GPU 加速、分布式训练等能力。但它们的底层运算逻辑,和 NumPy 是一脉相承的。

以 PyTorch 为例,把上面的 NumPy 代码翻译过去,几乎只需要把np换成torch,再加上requires_grad=True:

import torch Q = x @ W_q # 自动求导 scores = Q @ K.transpose(-2, -1) / torch.sqrt(torch.tensor(d_k)) weights = torch.softmax(scores, dim=-1) output = weights @ V

PyTorch 的softmax已经内置了数值稳定性处理,不需要手动减最大值。transpose(-2, -1)也比 NumPy 的transpose(0, 2, 1)更直观——用负数索引表示“最后两个维度互换”。

4.2 张量并行与显存优化

当你试图训练一个真正的 LLM 时,会发现显存不够用。一个 7B 参数的模型,光是参数就占 28GB(FP32),加上梯度、优化器状态、激活值,轻松超过 100GB。这时候就需要各种优化技术:

  • 混合精度训练:用 FP16 或 BF16 存储参数和计算,显存减半。
  • 梯度累积:用小 batch 多次前向传播,累积梯度后再更新,模拟大 batch。
  • 张量并行:把矩阵乘法拆到多张卡上,比如把W_q按列切分。
  • ZeRO 优化:把优化器状态、梯度、参数分片到多张卡上。

这些技术的底层,仍然是对张量 shape 的操作。比如张量并行里的列切分,就是把(d_model, d_model)的权重矩阵切成(d_model, d_model/num_gpus),每张卡算一部分,最后用all-reduce汇总。如果你理解 NumPy 的切片和拼接,这些概念就不难理解。

4.3 推理部署:ONNX 与量化

训练完之后,模型要部署到生产环境。这时候关注的重点从“训练速度”变成了“推理延迟”和“吞吐量”。常见的优化手段包括:

  • ONNX 导出:把 PyTorch 模型转成 ONNX 格式,然后用 ONNX Runtime 推理,跨平台兼容性好。
  • 量化:把 FP16 转成 INT8,模型体积缩小一半,推理速度提升,精度损失通常在可接受范围内。
  • KV Cache:自回归生成时,缓存之前 token 的 Key 和 Value,避免重复计算。

KV Cache 是一个很好的例子,它本质上是一个 shape 操作:把之前计算的 K 和 V 存起来,每次只计算新 token 的 K 和 V,然后拼接。用 NumPy 的话说就是np.concatenate([cache, new_k], axis=1)。理解了这个,再看推理框架的代码就不会觉得陌生。

5. 常见问题与排查技巧实录

5.1 Shape 不匹配:最常见的报错

Shape 不匹配是 LLM 开发中最常见的错误。我整理了一个速查表:

报错信息常见原因解决方法
matmul: Input operand 1 has a mismatch矩阵乘法维度不对检查a.shape[-1]是否等于b.shape[-2]
could not broadcast广播规则不满足从右往左对齐,确保维度相等或为 1
view size is not compatiblereshape 时元素总数不对计算np.prod(shape)是否一致
expected 4D input卷积层输入维度不对确认输入是(batch, channel, height, width)

我的经验是:在写代码之前,先在纸上画出每个张量的 shape。比如输入是(batch, seq_len, d_model),经过 Q 投影后是(batch, seq_len, d_k),经过注意力后是(batch, seq_len, d_v)。把这条链路画清楚,写代码时就不容易出错。

5.2 NumPy 版本不匹配

ModuleNotFoundError: No module named 'numpy'或者版本冲突,是新手经常遇到的问题。我的建议是:

  • 用虚拟环境:python -m venv myenv,然后source myenv/bin/activate(Linux/Mac)或myenv\Scripts\activate(Windows)。
  • 固定版本:在requirements.txt里写numpy==1.24.0,避免自动升级到不兼容的版本。
  • 检查依赖:pip list | grep numpy看看实际安装的版本。

如果遇到numpy和pandas、scipy的版本冲突,可以用pip install --upgrade --force-reinstall强制重装,或者用conda管理环境,它的依赖解析更靠谱。

5.3 梯度消失与梯度爆炸

训练 Transformer 时,梯度问题很常见。表现是 loss 不下降,或者突然变成 NaN。排查思路:

  1. 检查学习率:太大容易爆炸,太小容易消失。可以试试 warmup + cosine decay。
  2. 检查初始化:权重初始化太小会导致梯度消失,太大容易爆炸。常用 Xavier 或 Kaiming 初始化。
  3. 检查 LayerNorm:LayerNorm 能稳定训练,但如果位置放错(比如放在残差连接之后),效果会打折扣。
  4. 梯度裁剪:torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),防止梯度爆炸。

我踩过的一个坑是:忘记加 warmup。Transformer 训练初期,梯度很大,直接上大学习率容易发散。加上 warmup 之后,前几千步学习率从 0 线性增加到目标值,训练就稳定多了。

5.4 显存不足的排查与优化

显存不足(OOM)是训练大模型时的家常便饭。排查步骤:

  • 打印显存占用:torch.cuda.memory_allocated()看看当前用了多少。
  • 减小 batch size:最直接的方法,但可能影响训练效果,可以用梯度累积补偿。
  • 检查中间变量:有些中间变量没有及时释放,可以用del删除,然后torch.cuda.empty_cache()。
  • 用混合精度:torch.cuda.amp自动混合精度,显存减半,速度提升。
  • 检查模型并行:如果模型太大,单卡放不下,需要用张量并行或流水线并行。

我的经验是:先用小模型和小数据跑通流程,再逐步放大。一上来就上大模型,遇到 OOM 都不知道是哪里出了问题。小规模跑通之后,每一步的显存占用心里有数,放大时就知道该在哪里优化。

6. 学习路径与资源推荐

6.1 从 NumPy 到 Transformer 的路线图

如果你是从零开始,我建议按这个顺序走:

  1. NumPy 基础:数组创建、索引切片、广播、矩阵乘法、axis 操作。推荐官方教程和《Python 科学计算与数据科学应用》。
  2. 手写注意力机制:用 NumPy 实现单头和多头注意力,理解 Q、K、V 的含义。
  3. 手写完整 Transformer:包括位置编码、前馈网络、残差连接、LayerNorm。
  4. 迁移到 PyTorch:把 NumPy 代码翻译成 PyTorch,加上自动求导和 GPU 支持。
  5. 训练一个小模型:比如字符级语言模型,数据集用莎士比亚文本,感受完整的训练流程。
  6. 阅读开源代码:看 HuggingFace Transformers、nanoGPT 等项目的实现,学习工程技巧。

这个路线的好处是:每一步都有可运行的代码,每一步都能看到结果。不会出现“看了一堆论文但还是不会写代码”的情况。

6.2 值得反复看的资料

  • The Illustrated Transformer:用图解的方式讲注意力机制,非常适合入门。
  • nanoGPT:Karpathy 写的极简 GPT 实现,代码量少,适合精读。
  • HuggingFace Transformers:工业级实现,代码量大但注释详细,适合查阅。
  • LLM Wiki:社区维护的知识库,涵盖 LLM 的各个方面,适合系统学习。

我个人的习惯是:先看图解建立直觉,再看代码验证细节,最后自己动手写一遍。光看不动手,很容易产生“我懂了”的错觉,一写代码就卡壳。

6.3 一些实用的调试技巧

  • 打印 shape:在关键步骤后加print(x.shape),确认维度符合预期。
  • 用小数据:用batch=2, seq_len=4, d_model=8这样的小数据调试,出错时容易定位。
  • 单元测试:对每个模块写测试,比如注意力的输出 shape 是否正确,softmax 的权重和是否为 1。
  • 可视化:用 matplotlib 画出注意力权重,直观感受模型在关注什么。
  • 对比实现:自己写的和 PyTorch 内置的对比,确保数值一致。

我在实际使用中发现,调试 LLM 代码最有效的方法,是把大问题拆成小问题。比如注意力机制不工作,先单独测试 Q、K、V 的投影,再测试分数计算,再测试 softmax,一步步缩小范围。不要一上来就怀疑整个模型有问题。

最后再分享一个小技巧:如果你在学 Transformer 的时候觉得公式太难,不妨先把公式放一边,用 NumPy 把代码写出来,跑通之后再回头看公式,会发现很多符号其实就是在描述你写过的那些操作。代码是具体的,公式是抽象的,从具体到抽象,理解起来会顺畅很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:08:38

RAG文档解析瓶颈突破:Docling结构化解析实战指南

1. 为什么 RAG 的瓶颈从来不在模型,而在文档解析做过 RAG 项目的人都有一个共同体会:模型选型、向量库调参、提示词工程这些环节,折腾几天总能跑通,但真正让整个管线"翻车"的,往往是文档解析这一步。你拿一份…

作者头像 李华
网站建设 2026/9/30 5:08:16

人工智能期末速通:A*搜索、反向传播与大模型复习框架

1. 先搞清楚期末到底考什么,再决定背什么1.1 速通的第一步是画边界,不是打开第一页每到期末,我最怕看到的不是书厚,而是一堆人从第一章第一页开始往下翻。人工智能这门课的特殊性在于,它的知识密度极度不均匀&#xff…

作者头像 李华
网站建设 2026/9/30 5:08:16

AI布线不是替代工程师,而是数据驱动的PCB设计范式升级

1. 这不是“AI替代工程师”,而是布线逻辑的底层重写“规则已死!AI 布线终局是数据驱动”——这句话刚看到时,我手边正捏着一份刚被DRC报错27处的四层高速板设计稿。不是没按《高速信号线布线原则》操作,也不是忘了设置Altium里的等…

作者头像 李华
网站建设 2026/9/30 5:06:48

DeepSeekCoder-V2全面实战:环境搭建、参数调优与自动化编程案例解析

简介:DeepSeekCoder-V2作为备受关注的代码生成模型,正逐步改变开发者的工作方式。这份PDF文档系统梳理了从基础原理到高级技巧的完整学习路线,面向希望借助自动化编程提升开发效率的开发者、数据工作者及AI技术爱好者。资源共24页&#xff0c…

作者头像 李华
网站建设 2026/9/30 5:06:43

Gram-Schmidt正交化:从原理到QR分解与数值稳定性

1. 从“为什么需要正交化”说起我最早接触Gram-Schmidt正交化,是在学线性代数的时候。当时教材上公式写了一大堆,看起来就是一套机械的减法流程,既不觉得它美,也没觉得它有什么用。直到后来做数值计算,处理最小二乘拟合…

作者头像 李华
网站建设 2026/9/30 5:06:33

长尾难例才是AI数据集的核心战场

1. 为什么“平均样本”是数据集建设里最危险的幻觉“高质量数据集”这个词,最近两年被反复提起,几乎成了AI项目立项PPT里的标配词汇。但我在带三个CV方向落地项目时发现一个扎心的事实:团队花80%时间标注的,恰恰是模型最容易学、最…

作者头像 李华