image.png

摘要:你有没有好奇过,为什么把一段文字"扔进"大模型,它能理解语义、做推理、生成回答?答案的起点是:文字被表示成了向量,所有的理解和计算,本质上都是线性代数运算。本文从开发者视角出发,梳理理解大模型必须掌握的线性代数核心概念,并用 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 用一个贯穿例子理解整篇文章

我们用一句很简单的话作为贯穿例子:

“苹果手机很好用”

模型不会直接理解这句话。它大致会经历这样几步:

  1. 分词:把句子拆成 token,比如“苹果 / 手机 / 很 / 好用”。
  2. 向量化:每个 token 查表得到一个向量,比如“苹果”变成 [0.12, -0.08, ...]
  3. 矩阵变换:每层 Transformer 用权重矩阵不断改写这些向量,让它们融合上下文信息。
  4. 注意力计算:“苹果”在不同上下文中可能是水果,也可能是品牌,注意力会根据“手机”这个词调整它的含义。
  5. 输出预测:最后模型把处理后的向量转换成词表上的概率,预测下一个 token。

image.png

下面讲的所有线性代数概念,都可以放回这条流程里理解。


二、向量(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} v2=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| v1=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 ab=i=1naibi

点积有一个漂亮的几何解释——它等于两个向量的长度乘以它们夹角的余弦值:

a ⋅ b = ∥ a ∥ ⋅ ∥ b ∥ ⋅ cos ⁡ θ a \cdot b = \|a\| \cdot \|b\| \cdot \cos\theta ab=abcosθ

所以,如果我们把点积除以两个向量的长度,就能抵消"长度"的影响,只留下"方向"的信息,这就是余弦相似度

cosine_sim ( a , b ) = a ⋅ b ∥ a ∥ ⋅ ∥ b ∥ \text{cosine\_sim}(a, b) = \frac{a \cdot b}{\|a\| \cdot \|b\|} cosine_sim(a,b)=abab

余弦相似度的取值范围在 -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=kAikBkj

这个规则看起来繁琐,但有一个简洁的记忆方式:把两个矩阵的形状写在一起 —— ( 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(dk QKT)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) ΔWBA,其中rmin(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(dk QKT)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 QK
余弦相似度 衡量两向量方向的相似程度(-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 选多大

大模型视角

学完本文,几个以前听起来复杂的大模型概念,现在你能从数学层面理解了:

  1. 大模型本质是矩阵运算机器:所有的"理解"和"生成",底层都是向量和矩阵的线性变换,只是规模极大(数百层 × 每层多个矩阵运算)
  2. 参数量 = 所有权重矩阵的元素总数:7B 模型 ≈ 70亿个浮点数,分布在几百个矩阵里,每个矩阵都是 4096×4096 级别
  3. LoRA 省参数的秘密:微调时权重变化量是低秩的,用两个小矩阵乘积近似大矩阵,参数量压缩 128x,显存降低了同等比例
  4. RAG 检索的核心:余弦相似度让"表达不同但意思相近"的问题能找到正确文档,关键在于向量化后语义在空间中的距离关系

下一篇

大模型必备数学:概率论与信息论基础 —— 有了向量和矩阵,大模型能"理解"输入了。但它怎么决定输出哪个词?为什么训练大模型用的是"交叉熵损失"而不是其他损失函数?RLHF 里经常提到的 KL 散度是什么?温度参数 temperature=0.7 背后的数学是什么?下一篇全面解答。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐