大模型必备数学_线性代数核心概念速览

文章目录
摘要:你有没有好奇过,为什么把一段文字"扔进"大模型,它能理解语义、做推理、生成回答?答案的起点是:文字被表示成了向量,所有的理解和计算,本质上都是线性代数运算。本文从开发者视角出发,梳理理解大模型必须掌握的线性代数核心概念,并用 NumPy 代码直接验证每个公式,每个知识点都会明确告诉你它在大模型的哪个地方被用到。
前置知识:Python基础,会用NumPy
阅读时间:约30分钟
代码环境:Python 3.10+,numpy >= 1.24
如果你是第一次接触 AI 数学:这一篇不要求你把线性代数重新学成数学课。你只需要先抓住一条主线:大模型不能直接处理文字,所以先把文字变成向量;向量经过一层层矩阵变换,最后变成下一个 token 的概率。 本文出现的向量、矩阵、点积、SVD,都是为了理解这条主线服务的。
一、为什么开发者需要懂线性代数
很多人大学学过线性代数,但学完就忘,因为没有具体的使用场景支撑记忆。而大模型,恰好提供了一个极好的场景——线性代数在这里不是抽象的数学游戏,它就是大模型运转的"硬件电路"。
具体来说,以下这些你每天接触的大模型功能,直接依赖线性代数:
- Embedding(词嵌入):把每个 token 表示成一个高维向量(小模型可能是数百维,LLaMA-3 8B 等公开模型是 4096 维量级),文字才能被计算机理解和计算
- 注意力机制:Query、Key、Value 三个矩阵乘法决定了"这个词应该关注上下文的哪些词",是 Transformer 的核心
- 前向传播:每一层 Transformer 的计算,本质都是矩阵乘法加激活函数的叠加
- LoRA 微调:用两个小矩阵的乘积近似大矩阵,让在消费级 GPU 上微调 70B 大模型成为可能,核心原理是低秩分解
- RAG 向量检索:用余弦相似度在海量文档中找到最相关的片段,这就是向量的相似度计算
读完本文,这些概念对你来说将不再是听起来很厉害的词,而是你能动手实现、能解释原理的具体工具。
1.1 本篇先达到什么程度就够了
很多初学者读数学内容时会有一个误区:以为必须把每个公式都推导得很熟,才能继续学大模型。实际不是这样。对开发者来说,第一轮学习更重要的是建立“能看懂代码和结构图”的直觉。
读完这一篇,你先能回答下面 5 个问题,就已经达标:
- 为什么一句话进入模型前要先变成一串向量?
- 为什么大模型里到处都是矩阵乘法?
- 点积为什么能用来衡量两个 token 是否相关?
- 注意力公式里的 Q K T QK^T QKT 到底在算什么?
- LoRA 为什么能用两个小矩阵代替一个很大的权重改变量?
如果这些问题暂时不能完整回答,也没关系。先带着问题读完,后面每次看到 Embedding、Attention、LoRA、RAG 时再回头看对应小节,理解会自然补上。
1.2 用一个贯穿例子理解整篇文章
我们用一句很简单的话作为贯穿例子:
“苹果手机很好用”
模型不会直接理解这句话。它大致会经历这样几步:
- 分词:把句子拆成 token,比如“苹果 / 手机 / 很 / 好用”。
- 向量化:每个 token 查表得到一个向量,比如“苹果”变成
[0.12, -0.08, ...]。 - 矩阵变换:每层 Transformer 用权重矩阵不断改写这些向量,让它们融合上下文信息。
- 注意力计算:“苹果”在不同上下文中可能是水果,也可能是品牌,注意力会根据“手机”这个词调整它的含义。
- 输出预测:最后模型把处理后的向量转换成词表上的概率,预测下一个 token。

下面讲的所有线性代数概念,都可以放回这条流程里理解。
二、向量(Vector)
2.1 从"一个词"到"一组数字"
最直观的问题:计算机不理解文字,怎么让大模型"读懂"语言?
答案是:把每个词(或子词 token)转换成一组数字,即一个向量。向量是 n 个有序数字的组合,可以想象成 n 维空间中的一个坐标点。一个 3 维向量 [0.2, -0.5, 0.8] 就是空间中的一个点,或者等价地,从原点出发指向那个点的箭头。
在真实模型里,每个 token 的向量维度通常从数百到数千不等:小型 Transformer 可能只有 768 维,LLaMA-3 8B 这类公开大模型是 4096 维量级。维度越高,模型有更大的表示容量,但计算和显存成本也随之增加。
import numpy as np
# 一个3维向量,代表某个词的粗糙表示(实际大模型中是几千维)
word_vec = np.array([0.2, -0.5, 0.8])
print(f"向量维度: {word_vec.shape}") # (3,) 表示这是一个3维向量
print(f"向量: {word_vec}")
这段代码很简单,但背后的含义很深:word_vec.shape 输出 (3,) 意味着这是一个包含 3 个数的一维数组。在大模型里,词表中每个 token 都有这样一个向量与之对应,这些向量在训练过程中被不断调整,最终让语义相近的词在空间中聚集在一起——这就是 Embedding 的本质。
2.2 范数:向量的"长度"
知道了向量是空间中的点,我们很自然地想知道这个点离原点有多远,也就是向量的"长度"。这个"长度"在数学上叫做范数(Norm)。
最常用的是 L2 范数,也叫欧氏范数,就是我们熟悉的勾股定理推广:
∥ v ∥ 2 = v 1 2 + v 2 2 + ⋯ + v n 2 \|v\|_2 = \sqrt{v_1^2 + v_2^2 + \cdots + v_n^2} ∥v∥2=v12+v22+⋯+vn2
拆开来看:对向量的每个元素平方,加起来,再开方。对于 2D 向量 [3, 4],L2 范数就是 3 2 + 4 2 = 25 = 5 \sqrt{3^2 + 4^2} = \sqrt{25} = 5 32+42=25=5,正好是直角三角形的斜边长。
除了 L2 范数,还有 L1 范数,即各元素绝对值之和,更简单但用得少一些:
∥ v ∥ 1 = ∣ v 1 ∣ + ∣ v 2 ∣ + ⋯ + ∣ v n ∣ \|v\|_1 = |v_1| + |v_2| + \cdots + |v_n| ∥v∥1=∣v1∣+∣v2∣+⋯+∣vn∣
v = np.array([3.0, 4.0])
# L2 范数:开方勾股定理
l2_norm = np.linalg.norm(v) # 等价于 sqrt(3^2 + 4^2) = 5.0
print(f"L2 范数: {l2_norm}") # 5.0
# L1 范数:绝对值之和
l1_norm = np.linalg.norm(v, ord=1)
print(f"L1 范数: {l1_norm}") # 3 + 4 = 7.0
# 单位向量:把向量缩放到长度为1,方向不变
unit_v = v / l2_norm
print(f"单位向量: {unit_v}") # [0.6, 0.8]
print(f"单位向量的L2范数: {np.linalg.norm(unit_v):.4f}") # 验证:长度确实为1.0000
这里的单位向量操作叫做"归一化"——把一个向量除以它的长度,得到方向相同但长度为 1 的新向量。
在大模型中的应用:训练时用 L2 正则化防止权重过大;做向量相似度搜索之前通常要先归一化,这样比较的是"方向"而非"大小",语义检索更准确。
2.3 点积与余弦相似度:量化"相似"
现在有两个词向量,怎么判断它们语义是否相近?
最直觉的方法是看两个向量的方向是否一致——方向越接近,语义越相似。衡量方向相似性的工具叫余弦相似度,它的基础是点积(dot product)。
两个向量的点积定义为:对应位置的元素相乘,再全部加起来。
a ⋅ b = ∑ i = 1 n a i b i a \cdot b = \sum_{i=1}^{n} a_i b_i a⋅b=i=1∑naibi
点积有一个漂亮的几何解释——它等于两个向量的长度乘以它们夹角的余弦值:
a ⋅ b = ∥ a ∥ ⋅ ∥ b ∥ ⋅ cos θ a \cdot b = \|a\| \cdot \|b\| \cdot \cos\theta a⋅b=∥a∥⋅∥b∥⋅cosθ
所以,如果我们把点积除以两个向量的长度,就能抵消"长度"的影响,只留下"方向"的信息,这就是余弦相似度:
cosine_sim ( a , b ) = a ⋅ b ∥ a ∥ ⋅ ∥ b ∥ \text{cosine\_sim}(a, b) = \frac{a \cdot b}{\|a\| \cdot \|b\|} cosine_sim(a,b)=∥a∥⋅∥b∥a⋅b
余弦相似度的取值范围在 -1 到 1 之间。两个方向完全一致的向量余弦相似度为 1;方向完全相反为 -1;相互垂直(语义无关)为 0。
# 模拟三个词的 Embedding(简化为3维,真实情况是几千维)
king = np.array([0.9, 0.1, 0.2]) # "国王"的语义更接近权力、地位
queen = np.array([0.8, 0.2, 0.3]) # "女王"与"国王"语义相近
apple = np.array([0.1, 0.9, 0.1]) # "苹果"跟权力没关系,语义完全不同
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(f"king vs queen: {cosine_similarity(king, queen):.4f}") # 约0.98,非常相似
print(f"king vs apple: {cosine_similarity(king, apple):.4f}") # 约0.38,差异很大
print(f"queen vs apple: {cosine_similarity(queen, apple):.4f}") # 约0.46,差异很大
输出结果印证了直觉:king 和 queen 的余弦相似度接近 0.98,几乎"方向相同",而它们与 apple 的相似度只有 0.4 左右,差异明显。这就是 Embedding 模型学到的结果——语义相近的词,向量方向相近。
在大模型中的应用:RAG(检索增强生成)的检索步骤,就是把用户的问题编码成向量,然后在整个知识库的文档向量中用余弦相似度找最相关的片段,再把这些片段作为上下文喂给大模型。这就是向量数据库存在的意义。
三、矩阵(Matrix)
3.1 矩阵:批量处理向量的工具
向量只有一个维度(一排数字)。矩阵则是二维的数字表格——把向量排成若干行,就得到了矩阵。
更重要的是,矩阵是一种线性变换的表示。一个 m × n m \times n m×n 的矩阵 W W W,可以把一个 n n n 维向量变换成一个 m m m 维向量。这个变换是"线性"的:直线变换后还是直线,平行关系保持,只是坐标轴可能被旋转、拉伸或压缩。
在大模型里,权重矩阵就扮演着这种"空间变换器"的角色——它把输入向量从一个语义空间变换到另一个语义空间。
# 一个 2x3 的矩阵(2行3列)
# 它能把 3 维向量变换成 2 维向量
W = np.array([
[1, 2, 3],
[4, 5, 6]
])
print(f"矩阵形状: {W.shape}") # (2, 3) — 2行3列
3.2 矩阵乘法:大模型每一层都在做的事
矩阵乘法是大模型中出现频率最高的运算。两个矩阵 A A A( m × k m \times k m×k)和 B B B( k × n k \times n k×n)相乘,得到结果矩阵 C C C( m × n m \times n m×n)。
关键约束:第一个矩阵的列数必须等于第二个矩阵的行数,也就是中间那个维度 k k k 必须匹配。结果矩阵 C C C 的每个元素 C i j C_{ij} Cij,是 A A A 的第 i i i 行与 B B B 的第 j j j 列的点积:
C i j = ∑ k A i k B k j C_{ij} = \sum_{k} A_{ik} B_{kj} Cij=k∑AikBkj
这个规则看起来繁琐,但有一个简洁的记忆方式:把两个矩阵的形状写在一起 —— ( m , k ) ⋅ ( k , n ) → ( m , n ) (m,\,k)\cdot(k,\,n)\to(m,\,n) (m,k)⋅(k,n)→(m,n),中间的 k k k 必须相等并被"消掉",留下外侧的 m m m 和 n n n。注意这里的 ( m , k ) (m,k) (m,k)、 ( k , n ) (k,n) (k,n) 是形状而不是矩阵乘法本身, k k k 也不是一个新维度,只是一个匹配约束。
# 模拟 Transformer 前向传播的一步
# 场景:batch 中有 2 个句子,每句 3 个 token,每个 token 用 4 维向量表示
x = np.random.randn(2, 3, 4) # (batch_size=2, seq_len=3, hidden_dim=4)
# 这是一个线性变换层(类似 nn.Linear)的权重矩阵
# 把 4 维的 token 向量变换成 8 维
W = np.random.randn(4, 8) # (hidden_dim=4, output_dim=8)
b = np.random.randn(8) # 偏置项
# 执行线性变换:y = xW + b
# Python 中 @ 是矩阵乘法运算符,等价于 np.matmul
output = x @ W + b
print(f"输入形状: {x.shape}") # (2, 3, 4)
print(f"权重矩阵: {W.shape}") # (4, 8)
print(f"输出形状: {output.shape}") # (2, 3, 8) — 每个token的向量从4维变成了8维
注意形状的变化:输入是 (2, 3, 4),权重矩阵是 (4, 8),输出变成了 (2, 3, 8)。batch 维度和序列维度不变,只有最后的 hidden_dim 从 4 变成了 8。这个操作在 Transformer 的每一层都在发生,数百次。
在大模型中的应用:你见过的所有大模型"参数量"(7B、13B、70B),本质上就是这些权重矩阵中元素的总个数。一个 4096×4096 的矩阵就有 1600 万个参数,而一个典型的 7B 模型里有几百个这样的矩阵。
3.3 批量矩阵乘法:同时处理多个样本
大模型一次处理一个 batch 的数据(比如 32 条对话),而不是一条一条处理,这样效率更高。NumPy 和 PyTorch 都支持"批量矩阵乘法",即同时对 batch 中的每个样本做矩阵乘法:
# 模拟同时处理 4 个 attention head 的计算
# 每个 head 有自己的 Q、K 矩阵
batch_Q = np.random.randn(4, 10, 64) # (num_heads=4, seq_len=10, d_head=64)
batch_K = np.random.randn(4, 64, 10) # 注意:K已经转置,准备做点积
# 批量矩阵乘法:4 个 head 同时计算注意力分数
attention_scores = batch_Q @ batch_K # (4, 10, 10) — 每个 head 的注意力矩阵
print(f"注意力分数形状: {attention_scores.shape}")
# 输出 (4, 10, 10):4个头,每个头是10×10的"每个词关注其他词的分数矩阵"
@ 运算符作用在 3D 及以上的数组时,会自动对最后两个维度做矩阵乘法,其余维度视为 “batch”。这让多头注意力的计算可以高度并行化,这也是 GPU 特别适合训练 Transformer 的原因之一。
四、矩阵转置与注意力机制
转置(Transpose)操作很简单:把矩阵的行和列互换。原来在第 i i i 行第 j j j 列的元素,转置后变到第 j j j 行第 i i i 列。符号写作 A T A^T AT。
A = np.array([[1, 2, 3],
[4, 5, 6]]) # 形状 (2, 3)
print(f"原矩阵:\n{A}")
print(f"原矩阵形状: {A.shape}") # (2, 3)
print(f"\n转置矩阵:\n{A.T}")
print(f"转置后形状: {A.T.shape}") # (3, 2) — 行列交换
转置在大模型中的重要性,体现在 Transformer 注意力机制的核心公式里:
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dkQKT)V
这里的 K T K^T KT 就是转置。为什么需要转置?因为 Q Q Q 的形状是 (seq_len, d_k), K K K 也是 (seq_len, d_k),直接相乘维度不匹配。转置 K K K 为 (d_k, seq_len) 之后, Q × K T Q \times K^T Q×KT 就变成了 (seq_len, seq_len)——这是一个"每个词对其他词的相关性评分"矩阵,第 i i i 行第 j j j 列代表第 i i i 个词对第 j j j 个词的关注程度。
五、特征值与特征向量:矩阵变换的"不动方向"
对于一个方阵(行列数相等的矩阵) A A A,大多数向量经过矩阵变换后会改变方向。但有一些特殊的向量,经过矩阵变换后方向不变,只是"缩放"了一个倍数。这些特殊向量叫做特征向量,对应的缩放倍数叫做特征值。
数学上,如果存在非零向量 v v v 和标量 λ \lambda λ,满足:
A v = λ v Av = \lambda v Av=λv
那么 v v v 是矩阵 A A A 的特征向量, λ \lambda λ 是对应的特征值。
直觉理解:矩阵 A A A 是一种空间变换,大多数向量经过这个变换后会"转向"。但特征向量很特殊,它只会沿着自身的方向被拉伸或压缩(拉伸多少倍,就是特征值)。如果特征值为 2,意味着这个方向的向量被放大到 2 倍;特征值为 -1 意味着方向被翻转。
A = np.array([[3, 1],
[1, 3]])
eigenvalues, eigenvectors = np.linalg.eig(A)
print(f"特征值: {eigenvalues}") # [4. 2.]
print(f"特征向量(每列是一个):\n{eigenvectors}")
# 验证 Av = λv:对第一个特征向量验证
v0 = eigenvectors[:, 0] # 取第一列,即第一个特征向量
lambda0 = eigenvalues[0] # 对应的特征值是 4
print(f"\n验证 Av = λv:")
print(f" Av = {A @ v0.round(4)}") # 矩阵乘以特征向量
print(f" λv = {(lambda0 * v0).round(4)}") # 特征值乘以特征向量
print(f" 两者相等: {np.allclose(A @ v0, lambda0 * v0)}") # 应该为 True
结果显示 A v Av Av 和 λ v \lambda v λv 完全相同,印证了特征向量只是被缩放、没有转向。
在大模型中的应用:特征值分解是 SVD(奇异值分解)的基础,而 SVD 是理解 LoRA 的关键数学工具,下面重点介绍。
六、奇异值分解(SVD):LoRA 的理论基础
6.1 SVD 是什么
特征值分解只适用于方阵,而 SVD(Singular Value Decomposition,奇异值分解)可以对任意矩阵做分解。这是线性代数中最强大的工具之一。
对于任意 m × n m \times n m×n 矩阵 W W W,SVD 把它分解为三个矩阵的乘积:
W = U Σ V T W = U \Sigma V^T W=UΣVT
三个矩阵各有含义:
- U U U( m × m m \times m m×m):左奇异向量矩阵,每一列是 W W W 输出空间的"方向"
- Σ \Sigma Σ( m × n m \times n m×n):对角矩阵,对角线上的值叫奇异值,从大到小排列,代表每个方向的"重要程度"
- V T V^T VT( n × n n \times n n×n):右奇异向量矩阵的转置,每一列是 W W W 输入空间的"方向"
更直观地理解:奇异值越大,对应方向携带的信息越重要;奇异值接近 0,对应方向几乎不含有效信息,可以被舍弃。
# 构造一个有明显低秩特征的矩阵
W = np.array([
[1, 2, 3, 4],
[2, 4, 6, 8], # 这一行 = 第一行 × 2,说明矩阵有线性依赖
[1, 1, 1, 1]
], dtype=float)
# 对 W 做 SVD 分解
U, sigma, Vt = np.linalg.svd(W, full_matrices=False)
print(f"奇异值: {sigma.round(4)}") # 第一个奇异值远大于其他,说明矩阵信息集中在第一个方向
print(f"U 形状: {U.shape}") # (3, 3)
print(f"sigma 形状: {sigma.shape}") # (3,) — 3个奇异值(取min(3,4))
print(f"Vt 形状: {Vt.shape}") # (3, 4)
# 用最大的1个奇异值重建矩阵(秩-1近似)
r = 1
W_approx = U[:, :r] @ np.diag(sigma[:r]) @ Vt[:r, :]
print(f"\n原矩阵:\n{W}")
print(f"\n只保留1个奇异值的近似矩阵:\n{W_approx.round(4)}")
print(f"\n近似误差(Frobenius范数): {np.linalg.norm(W - W_approx):.6f}")
# 误差接近0,说明这个矩阵的信息几乎全在第一个奇异值方向上
看奇异值的分布就很有意思:第一个奇异值远大于后面的,说明这个矩阵的信息高度集中在一个方向上,用一个方向就能近似还原整个矩阵,误差极小。这就是低秩矩阵的特征。
6.2 为什么这和 LoRA 有关
LoRA(Low-Rank Adaptation,低秩适配)的核心发现是:大模型在微调时,权重矩阵的改变量 Δ W \Delta W ΔW 具有低秩结构。
这意味着什么?意味着虽然 Δ W \Delta W ΔW 在形式上是一个 4096 × 4096 4096 \times 4096 4096×4096 的大矩阵(1600万个参数),但它真正携带的信息可能只需要很少几个"方向"就能描述清楚,也就是说它的秩很低。
利用这个特性,LoRA 的做法是:不直接更新 Δ W \Delta W ΔW,而是用两个小矩阵 B B B( m × r m \times r m×r)和 A A A( r × n r \times n r×n)的乘积来近似它:
Δ W ≈ B A , 其中 r ≪ min ( m , n ) \Delta W \approx BA, \quad \text{其中} \quad r \ll \min(m, n) ΔW≈BA,其中r≪min(m,n)
这里的 r r r 叫做 LoRA 的"秩"(rank),通常取 4、8、16、32 这样的小值。参数量的压缩效果是惊人的:
# LoRA 参数量对比:以 LLaMA-3 中常见的矩阵尺寸为例
m, n = 4096, 4096 # 一个典型的权重矩阵:4096 × 4096
full_params = m * n # 全参数微调需要更新的参数量
print("LoRA 参数压缩效果:")
print(f"原矩阵参数量: {full_params:,} 个参数")
print()
for r in [4, 8, 16, 32, 64]:
# LoRA 只需训练 B(m×r)和 A(r×n)
lora_params = m * r + r * n
compression = full_params / lora_params
print(f" r={r:2d}: LoRA参数 = {lora_params:7,},比全参数压缩了 {compression:.0f}x")
输出结果:
原矩阵参数量: 16,777,216 个参数
r= 4: LoRA参数 = 32,768,比全参数压缩了 512x
r= 8: LoRA参数 = 65,536,比全参数压缩了 256x
r=16: LoRA参数 = 131,072,比全参数压缩了 128x
r=32: LoRA参数 = 262,144,比全参数压缩了 64x
r=64: LoRA参数 = 524,288,比全参数压缩了 32x
r=16 时参数量只有原来的 1/128,这就是为什么消费级显卡(如 24GB 的 RTX 4090)也能微调 70B 参数的大模型——需要更新的参数少了几百倍,显存需求也大幅下降。
七、矩阵的秩:信息维度的度量
学完 SVD 之后,矩阵的秩就很好理解了:秩 = 非零奇异值的数量 = 矩阵真正携带信息的维度数。
可以把矩阵想象成一个信息压缩器。一个 100 × 100 100 \times 100 100×100 的矩阵理论上可以有 100 的秩,但如果它的奇异值只有前 5 个非零,说明这个矩阵的信息只在 5 个方向上,秩就是 5,其余 95 个维度都是冗余或噪声。
- 满秩矩阵:秩等于 min ( m , n ) \min(m, n) min(m,n),所有维度都有信息,最难压缩
- 低秩矩阵:秩远小于 min ( m , n ) \min(m, n) min(m,n),信息高度集中,可以大幅压缩
# 用几个具体例子感受秩的含义
# 满秩矩阵:每一列(或行)都线性无关,没有冗余
A_full = np.array([[1, 0],
[0, 1]]) # 单位矩阵,两个方向完全独立
print(f"单位矩阵的秩: {np.linalg.matrix_rank(A_full)}") # 2 = min(2,2),满秩
# 秩为1的矩阵:第二行是第一行的倍数,没有新信息
A_rank1 = np.array([[1, 2],
[2, 4]]) # 第二行 = 2 × 第一行,完全重复
print(f"行线性相关矩阵的秩: {np.linalg.matrix_rank(A_rank1)}") # 1
# 验证:秩 = 非零奇异值的数量
for name, A in [("单位矩阵", A_full), ("低秩矩阵", A_rank1)]:
_, sigma, _ = np.linalg.svd(A)
nonzero_count = np.sum(sigma > 1e-10)
print(f" {name}: 奇异值 = {sigma.round(4)}, 非零个数 = {nonzero_count}")
从奇异值来看,低秩矩阵的第二个奇异值是 0(或接近 0),说明第二个方向完全没有信息,可以被丢弃。这就是低秩近似的理论依据。
八、综合示例:用线性代数手写注意力机制
现在把以上所有知识串联起来,手写一遍 Scaled Dot-Product Attention:
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dkQKT)V
这个公式初看有点复杂,但拆开来每一步都是我们学过的操作。让我们一步步解释并实现:
import numpy as np
def softmax(x, axis=-1):
# 减去最大值是数值稳定性技巧,防止 exp(大数) 溢出
# 这个技巧不改变结果,但防止了浮点数溢出的问题
e_x = np.exp(x - x.max(axis=axis, keepdims=True))
return e_x / e_x.sum(axis=axis, keepdims=True)
def scaled_dot_product_attention(Q, K, V):
"""
注意力机制的完整实现,每步都有解释
Q(Query,查询):当前词在"提问"——我想关注什么?
K(Key,键):每个词在"回答"——我是什么?
V(Value,值):每个词的实际内容——如果你关注我,你会得到什么?
形状:Q、K 是 (seq_len, d_k),V 是 (seq_len, d_v)
"""
d_k = Q.shape[-1] # 向量维度,用于缩放
# 步骤1:Q 和 K 做点积,计算"相关性分数"
# Q @ K.T 的形状是 (seq_len, seq_len)
# 结果的第 i 行第 j 列,是第 i 个词的 Query 与第 j 个词的 Key 的点积
# 数值越大,说明这两个词"越相关"
scores = Q @ K.T
# 步骤2:除以 sqrt(d_k) 进行缩放
# 原因:当 d_k 很大时,点积的方差会变大,softmax 的梯度会变小(梯度消失)
# 除以 sqrt(d_k) 可以把方差稳定在合适范围
scores = scores / np.sqrt(d_k)
# 步骤3:Softmax,把分数转换为概率分布(权重)
# 每一行的权重之和为 1,代表当前词把 100% 的"注意力"分配给了各个词
weights = softmax(scores) # 形状仍然是 (seq_len, seq_len)
# 步骤4:用权重对 Value 做加权求和
# 当前词的输出 = 它对所有词的关注权重 × 那些词的 Value 的加权平均
output = weights @ V # (seq_len, seq_len) @ (seq_len, d_v) = (seq_len, d_v)
return output, weights
# 模拟一个4个词的短句子
seq_len = 4 # 4个词
d_k = 8 # Q、K 的向量维度
d_v = 8 # V 的向量维度
np.random.seed(0)
Q = np.random.randn(seq_len, d_k)
K = np.random.randn(seq_len, d_k)
V = np.random.randn(seq_len, d_v)
output, weights = scaled_dot_product_attention(Q, K, V)
print(f"输入序列长度: {seq_len} 个词,每个词 {d_k} 维向量")
print(f"注意力输出形状: {output.shape}") # (4, 8),形状与输入相同
print(f"\n第1个词的注意力权重(它关注各词的程度):")
for i, w in enumerate(weights[0]):
bar = "█" * int(w * 40)
print(f" 关注第{i+1}个词: {w:.4f} {bar}")
print(f" 权重之和验证: {weights[0].sum():.4f}") # 必须为1.0
运行这段代码,你能清楚地看到第 1 个词是如何把注意力分配到序列中各个位置的。权重加起来等于 1.0,验证了 softmax 保证了"注意力总量恒定"。这就是 Transformer 的核心机制,所谓"Self-Attention(自注意力)",就是每个词同时扮演 Query、Key、Value 的角色,计算自己对整个序列的注意力。
九、总结
本文涵盖了理解大模型所需的核心线性代数概念。用一张表格做个回顾:
| 概念 | 核心含义 | 在大模型中的具体应用 |
|---|---|---|
| 向量 | n维空间中的点,代表方向和大小 | Token Embedding:每个词是一个高维向量 |
| L2范数 | 向量的"长度" | 正则化防止权重过大;归一化后才能做余弦检索 |
| 点积 | 两向量对应元素乘积的和 | 注意力分数计算: Q ⋅ K Q \cdot K Q⋅K |
| 余弦相似度 | 衡量两向量方向的相似程度(-1到1) | RAG 向量检索;语义相似度比较 |
| 矩阵乘法 | ( m × k ) × ( k × n ) = ( m × n ) (m \times k) \times (k \times n) = (m \times n) (m×k)×(k×n)=(m×n) | 注意力计算、前向传播每一层的线性变换 |
| 转置 | 行列互换 | 注意力公式中的 K T K^T KT |
| SVD | 任意矩阵分解为 U Σ V T U\Sigma V^T UΣVT | LoRA 的理论基础:权重变化可以低秩近似 |
| 矩阵的秩 | 矩阵非冗余信息的维度数 | 衡量权重矩阵的可压缩程度,决定 LoRA 的 r 选多大 |
大模型视角
学完本文,几个以前听起来复杂的大模型概念,现在你能从数学层面理解了:
- 大模型本质是矩阵运算机器:所有的"理解"和"生成",底层都是向量和矩阵的线性变换,只是规模极大(数百层 × 每层多个矩阵运算)
- 参数量 = 所有权重矩阵的元素总数:7B 模型 ≈ 70亿个浮点数,分布在几百个矩阵里,每个矩阵都是 4096×4096 级别
- LoRA 省参数的秘密:微调时权重变化量是低秩的,用两个小矩阵乘积近似大矩阵,参数量压缩 128x,显存降低了同等比例
- RAG 检索的核心:余弦相似度让"表达不同但意思相近"的问题能找到正确文档,关键在于向量化后语义在空间中的距离关系
下一篇
大模型必备数学:概率论与信息论基础 —— 有了向量和矩阵,大模型能"理解"输入了。但它怎么决定输出哪个词?为什么训练大模型用的是"交叉熵损失"而不是其他损失函数?RLHF 里经常提到的 KL 散度是什么?温度参数 temperature=0.7 背后的数学是什么?下一篇全面解答。
更多推荐




所有评论(0)