Skip to content

第 1 章 线性代数

本章目标:建立大模型所需的全部线性代数直觉——从向量、矩阵到 SVD,并用纯 NumPy 手写一个神经网络线性层。

预计用时:3~5 天(每天 1~2 小时)

学完你将能够

  • 说清楚标量、向量、矩阵、张量是什么,以及为什么"embedding 就是一个向量"
  • 用 NumPy 计算点积、范数、余弦相似度,并理解它们如何支撑语义检索(RAG)
  • 用三种视角理解矩阵乘法,看懂神经网络每一层 $y = Wx + b$ 的本质
  • 直觉理解特征值、特征向量与 SVD,知道低秩近似为什么是 LoRA 微调的数学根基
  • 理解为什么 GPU 擅长矩阵乘法(算术强度的萌芽)
  • 手写 y = softmax(Wx + b) 并跑通一次前向计算

1.1 为什么大模型工程师必须懂线性代数

先泼一盆冷水,再给一颗糖。

冷水:大模型的全部计算,归根结底就是一件事——大规模矩阵乘法。一个 7B 模型的一次推理,背后是几千亿次乘加运算,而它们几乎全部以"矩阵乘矩阵"的形式组织。不懂线性代数,你看模型代码就像看天书。

:你需要的不是数学系那套证明推导,而是直觉 + 会用工具算。本章我们遵循三条原则:

  1. 几何直觉优先:每个概念先画图、打比方,再上公式。
  2. 公式够用就好:只讲大模型真正用到的部分,不死磕证明。
  3. 每学一个知识点,立刻问:→ 这个在大模型里用在哪?

运行本章代码只需要 NumPy(第 0 章的环境里安装):

bash
pip install numpy
python
import numpy as np

# 固定随机种子,保证你和我的运行结果一致(好习惯,实验可复现)
np.random.seed(42)
print(np.__version__)

1.2 标量、向量、矩阵、张量

1.2.1 四个概念,一个家族

这四个词听起来唬人,其实就是"装数字的容器",按维度从小到大排列:

名称维度类比例子数学记号
标量(scalar)0 维温度计上的一个读数学习率 0.001$a \in \mathbb{R}$
向量(vector)1 维一排格子,每格一个数一个词的 embedding$\mathbf{x} \in \mathbb{R}^{n}$
矩阵(matrix)2 维Excel 表格一层网络的权重$W \in \mathbb{R}^{m \times n}$
张量(tensor)任意维一摞 Excel 表格叠起来一批句子的 embedding$\mathcal{T} \in \mathbb{R}^{b \times m \times n}$

符号说明:$\mathbb{R}$ 表示"全体实数"(就是你能想到的普通数,如 3.14、-2、0)。$\mathbb{R}^{m \times n}$ 表示"由实数组成的 m 行 n 列矩阵"。粗体小写字母(如 $\mathbf{x}$)习惯表示向量,大写字母(如 $W$)表示矩阵。

一句话记忆:标量是一个数,向量是一排数,矩阵是一张表,张量是一摞表。

python
import numpy as np

# 标量:0 维数组——就是一个数
scalar = np.array(3.14)
print("标量:", scalar, "| 形状:", scalar.shape)        # 形状: ()

# 向量:1 维数组——一排数
vector = np.array([1.0, 2.0, 3.0])
print("向量:", vector, "| 形状:", vector.shape)        # 形状: (3,)

# 矩阵:2 维数组——3 行 4 列的表
matrix = np.array([[1, 2, 3, 4],
                   [5, 6, 7, 8],
                   [9, 10, 11, 12]])
print("矩阵形状:", matrix.shape)                        # 形状: (3, 4)

# 张量:3 维数组——可以想象成"2 张 3x4 的表叠在一起"
tensor = np.zeros((2, 3, 4))
print("张量形状:", tensor.shape)                        # 形状: (2, 3, 4)

💡 经验法则:在深度学习里,.shape(形状)是你最好的朋友。任何代码看不懂,先打印每一步的 .shape。形状对不上,是新手 90% 的报错来源。

1.2.2 向量的几何直觉:空间中的一个"箭头"

一个 n 维向量,可以想象成 n 维空间里从原点指向某点的箭头

  • 2 维向量 $[3, 4]$:平面上指向"右 3、上 4"那个点的箭头。
  • 3 维向量 $[1, 2, 3]$:立体空间里的一个箭头。
  • 768 维向量:……画不出来,但数学上完全同理。

人类画不出高维空间,但请记住:低维空间里的几何直觉(方向、长度、夹角)在高维空间依然成立。这是整章的思维支点。

1.2.3 为什么"embedding 就是一个向量"

大模型不认识文字,只认识数字。它做的第一件事,就是把每个词(token)通过一张巨大的"查词表"转换成一个向量——这就是 embedding(词嵌入)

类比:embedding 就像给每个词一个"身份证号码",但不是 18 位,而是几百到几千位(比如 Qwen3-0.6B 用 1024 维,GPT-3 用 12288 维)。

关键在于:这个向量不是乱编的,而是训练出来的,因此语义相近的词,向量在空间中的方向也相近。"国王"和"女王"的向量很接近,"国王"和"香蕉"的向量离得很远。

        ↑ "女王"
        |  ↗ "国王"        (语义相近 → 方向相近)
        |
        |
  ------+----------------→
        |                    ↘ "香蕉"(语义无关 → 方向差很远)

这个在大模型里用在哪:embedding 是大模型的"文字入口"。第 4 章我们会亲手从模型里取出 embedding 矩阵;本章 1.3 节的余弦相似度,就是度量"两个词向量方向有多接近"的标准工具。

📝 练习 1.1:用 NumPy 创建一个形状为 (50000, 1024) 的随机矩阵,想象它是"5 万个词、每个词 1024 维"的 embedding 表。回答:这张表一共有多少个数字?如果用 FP32(每数 4 字节)存储,占多少内存?(提示:回忆第 0 章的显存估算公式。)


1.3 向量运算:加减、数乘、点积、范数、余弦相似度

1.3.1 加减与数乘:对应位置做运算

向量加减就是对应位置的数分别相加减,数乘就是每个数都乘以同一个标量

  • $\mathbf{a} + \mathbf{b} = [a_1+b_1,\ a_2+b_2,\ \dots,\ a_n+b_n]$
  • $c \cdot \mathbf{a} = [c \cdot a_1,\ c \cdot a_2,\ \dots,\ c \cdot a_n]$

几何上:向量相加是"两个箭头首尾相接",数乘是"把箭头拉长或缩短"。

python
import numpy as np

a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])

print(a + b)    # [5. 7. 9.] —— 逐元素相加
print(a - b)    # [-3. -3. -3.] —— 逐元素相减
print(2.5 * a)  # [ 2.5  5.   7.5] —— 数乘:每个元素乘 2.5

这个在大模型里用在哪:经典的"词向量算术"——国王 - 男人 + 女人 ≈ 女王。训练时,梯度(一个向量)乘以学习率(一个标量)再加到权重上,本质就是数乘和加法:$\theta \leftarrow \theta - \eta \nabla$。

1.3.2 点积(内积):两个向量"有多配合"

**点积(dot product,也叫内积)**是两个向量对应位置相乘再求和:

$$\mathbf{a} \cdot \mathbf{b} = a_1 b_1 + a_2 b_2 + \dots + a_n b_n = \sum_{i=1}^{n} a_i b_i$$

注意:两个向量做点积,结果是一个标量(一个数),不是向量。

直觉:点积度量两个向量"方向有多一致"。想象两人在拔河的两端使力:

  • 方向完全相同 → 点积最大(正数)——"劲儿往一处使"
  • 方向垂直 → 点积为 0——"互不相关"
  • 方向相反 → 点积为负数——"互相抵消"
python
import numpy as np

a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])

# 计算点积的三种等价写法(结果完全相同)
print(a @ b)          # 32.0 —— 推荐写法,@ 是 NumPy 的矩阵/向量乘运算符
print(np.dot(a, b))   # 32.0 —— 显式函数写法
print(np.sum(a * b))  # 32.0 —— 手动版:先逐元素乘,再求和(帮助理解定义)
# 验证:1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32

这个在大模型里用在哪:点积是全书出现频率最高的运算!注意力机制(第 4 章核心)的"注意力分数"就是 Query 向量和 Key 向量的点积——两个 token 的向量越"配合",它们互相注意的程度越高。

1.3.3 范数:向量的"长度"

**范数(norm)**就是向量的长度。最常用的两种:

L2 范数(欧几里得长度):各分量平方和开根号,就是勾股定理的推广:

$$|\mathbf{a}|_2 = \sqrt{a_1^2 + a_2^2 + \dots + a_n^2}$$

二维情形 $\sqrt{a_1^2 + a_2^2}$ 就是直角三角形斜边长度——你高中就会了。

L1 范数(曼哈顿长度):各分量绝对值之和:

$$|\mathbf{a}|_1 = |a_1| + |a_2| + \dots + |a_n|$$

类比:L2 是"直线飞过去"的直线距离,L1 是"在棋盘格街道上只能横平竖直走"的出租车距离。

python
import numpy as np

a = np.array([3.0, 4.0])

print(np.linalg.norm(a))          # 5.0 —— 默认就是 L2 范数(勾股定理:3-4-5 三角形)
print(np.linalg.norm(a, ord=2))   # 5.0 —— 显式指定 L2
print(np.linalg.norm(a, ord=1))   # 7.0 —— L1 范数:|3| + |4| = 7

这个在大模型里用在哪:① 训练时常见"梯度裁剪"(gradient clipping),就是当梯度向量的 L2 范数超过阈值时把它缩短,防止梯度爆炸;② 某些正则化(L1/L2 正则)直接把参数范数加进损失函数;③ 下一节的余弦相似度离不开 L2 范数。

1.3.4 余弦相似度:语义检索的数学核心

点积有个缺点:它同时受"方向"和"长度"影响。两个向量方向完全一致但一个特别长,点积也会很大。而判断语义是否相近,我们只关心方向,不关心长度。

**余弦相似度(cosine similarity)**把长度因素除掉,只留方向:

$$\text{cos_sim}(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{|\mathbf{a}|_2 \cdot |\mathbf{b}|_2} = \cos\theta$$

其中 $\theta$ 是两个向量的夹角。结果的取值范围是 $[-1, 1]$:

  • $1$:方向完全相同(夹角 0°)→ 语义完全一致
  • $0$:垂直(夹角 90°)→ 语义无关
  • $-1$:方向完全相反(夹角 180°)

类比:点积像"合作的总产出"(既看默契又看体格),余弦相似度像"默契程度"(只看配合度,不看体格)。

1.3.5 完整例子:手工构造"句子向量"做语义检索

下面的例子模拟一个迷你版 RAG(检索增强生成):假设我们只有 4 个语义维度(分别代表"与猫相关、与狗相关、与汽车相关、与编程相关"),手工构造几个句子向量,然后对一个问题检索最相关的文档:

python
import numpy as np

def cosine_similarity(a, b):
    """计算两个向量的余弦相似度"""
    return (a @ b) / (np.linalg.norm(a) * np.linalg.norm(b))

# ---- 手工构造"句子向量"(真实场景中是模型生成的,这里用手工值演示原理)----
# 4 个维度分别代表:[猫, 狗, 汽车, 编程] 四种语义的含量
doc1 = np.array([0.9, 0.1, 0.0, 0.0])   # 文档1:"我家养了一只可爱的猫"
doc2 = np.array([0.1, 0.9, 0.1, 0.0])   # 文档2:"遛狗需要注意什么"
doc3 = np.array([0.0, 0.0, 0.9, 0.1])   # 文档3:"新能源汽车销量大涨"
query = np.array([0.85, 0.15, 0.0, 0.0]) # 问题:"猫咪怎么喂养?"——和文档1方向几乎一致

# ---- 分别计算问题与每篇文档的余弦相似度 ----
print("问题 vs 文档1(猫):", cosine_similarity(query, doc1))  # ≈ 0.99,最相关
print("问题 vs 文档2(狗):", cosine_similarity(query, doc2))  # ≈ 0.35,略相关(都是宠物)
print("问题 vs 文档3(车):", cosine_similarity(query, doc3))  # 0.00,完全无关

# ---- 工程实践:先归一化,之后检索只需一次点积 ----
# 归一化 = 把向量长度变成 1(方向不变)
docs = np.stack([doc1, doc2, doc3])           # 文档库:(3, 4) 矩阵
docs_norm = docs / np.linalg.norm(docs, axis=1, keepdims=True)  # 每行除以自己的 L2 范数
query_norm = query / np.linalg.norm(query)

# 归一化之后,点积 == 余弦相似度,一次矩阵乘法算完整个文档库
scores = docs_norm @ query_norm               # 形状 (3,)
print("检索得分:", scores)
print("最相关文档编号:", np.argmax(scores))    # argmax 返回最大值的索引

这个在大模型里用在哪:这就是 RAG 和向量数据库(如 FAISS、Milvus)的核心原理。真实系统中,句子向量由 embedding 模型生成(几百上千维),文档库有百万千万条,但数学上和你上面写的代码一模一样。第 7 章做 RAG 实战时你会再次见到它。

📝 练习 1.2:在上面的例子中加入第 4 篇文档 doc4 = np.array([0.0, 0.1, 0.0, 0.9])("Python 编程入门教程"),重新计算检索得分并排序。再把 query 的长度放大 10 倍(方向不变),观察余弦相似度是否变化——验证"余弦相似度只看方向"。

💡 经验法则:工程实践中,embedding 向量入库前先归一化,之后检索就退化为一次矩阵乘法,速度极快。这是向量数据库能毫秒级检索亿级向量的秘诀之一。


1.4 矩阵运算:神经网络的本体

1.4.1 矩阵乘法的三种理解视角

矩阵乘法是本章最重要的概念,我们给三种等价视角,按场景换着用。

设 $\mathbf{x} \in \mathbb{R}^{n}$ 是输入向量,$W \in \mathbb{R}^{m \times n}$ 是权重矩阵,$W\mathbf{x}$ 的结果是一个 $m$ 维向量。

视角一:线性变换(几何视角)。矩阵乘向量 = 把这个向量旋转 + 拉伸到另一个空间。比如矩阵 $W = \begin{bmatrix} 0 & -1 \ 1 & 0 \end{bmatrix}$ 把任何 2 维向量逆时针旋转 90°。神经网络每一层,就是把数据"变换"到一个更好用的表示空间。

视角二:行点积列(计算视角)。$W\mathbf{x}$ 结果的第 $i$ 个元素 = $W$ 的第 $i$ 与 $\mathbf{x}$ 的点积

$$ \begin{bmatrix} w_{11} & w_{12} \ w_{21} & w_{22} \ w_{31} & w_{32} \end{bmatrix} \begin{bmatrix} x_1 \ x_2 \end{bmatrix} = \begin{bmatrix} w_{11}x_1 + w_{12}x_2 \ w_{21}x_1 + w_{22}x_2 \ w_{31}x_1 + w_{32}x_2 \end{bmatrix} $$

也就是说,每一行是一个"探测器",点积度量输入与这个探测器的匹配程度。m 个输出 = m 个探测器各自的打分。

视角三:批量向量变换(工程视角)。矩阵乘矩阵 $WX$:把 $X$ 的每一列看作一个向量,一次性全部变换。这就是深度学习中"批量(batch)处理"的本质——一次算 64 个样本,比循环 64 次快得多。

python
import numpy as np

W = np.array([[1.0, 0.0],
              [0.0, 2.0],
              [1.0, 1.0]])   # (3, 2):三个"探测器",每个探测 2 维输入
x = np.array([3.0, 4.0])     # (2,):一个输入向量

# ---- 视角一/二的结果:矩阵乘向量 ----
y = W @ x                    # 形状 (3,)
print("W @ x =", y)          # [3. 8. 7.]

# 手动验证视角二(第 i 个输出 = 第 i 行与 x 的点积)
print("第 0 行点积:", W[0] @ x)   # 1*3 + 0*4 = 3
print("第 1 行点积:", W[1] @ x)   # 0*3 + 2*4 = 8
print("第 2 行点积:", W[2] @ x)   # 1*3 + 1*4 = 7

# ---- 视角三:批量处理,一次变换 4 个输入 ----
X = np.array([[3.0, 1.0, 0.0, 2.0],
              [4.0, 1.0, 1.0, 0.0]])   # (2, 4):每列是一个样本,共 4 个样本
Y = W @ X                              # (3, 2) @ (2, 4) -> (3, 4)
print("批量结果形状:", Y.shape)
print("第一个样本的结果:", Y[:, 0])     # 与上面 W @ x 相同:[3. 8. 7.]

形状铁律:$(m \times n) @ (n \times k) \rightarrow (m \times k)$——中间的两个 $n$ 必须相等,"消掉"后剩下 $m$ 和 $k$。

1.4.2 神经网络每一层:y = Wx + b

现在你可以看懂神经网络最基本的公式了:

$$\mathbf{y} = W\mathbf{x} + \mathbf{b}$$

  • $\mathbf{x} \in \mathbb{R}^{n}$:输入(比如一个 token 的 1024 维 embedding)
  • $W \in \mathbb{R}^{m \times n}$:权重矩阵,就是训练要学习的参数
  • $\mathbf{b} \in \mathbb{R}^{m}$:偏置向量,给每个输出加一个可学习的"基础分"
  • $\mathbf{y} \in \mathbb{R}^{m}$:输出

以 Qwen3-0.6B 为例,它的隐藏维度是 1024,其中一层的权重矩阵就是 $1024 \times 1024$ 或 $1024 \times 3072$ 的矩阵——一个矩阵就是一层的全部"知识"。几十层叠起来,就是大模型。

这个在大模型里用在哪:翻开任何大模型的代码,Linearq_projk_projv_projo_projgate_projup_projdown_proj……这些全都是一个 $W$ 矩阵加一个(可选的)$\mathbf{b}$。第 4 章我们会逐个解剖。

1.4.3 转置、单位矩阵、逆矩阵

转置(transpose):把矩阵的行和列互换,记作 $W^T$。$(m \times n)$ 转置后变成 $(n \times m)$。类比:把 Excel 表格"沿对角线翻个面"。

单位矩阵(identity matrix)$I$:对角线是 1、其余全是 0 的方阵。它是矩阵世界的"数字 1":任何矩阵乘它都不变,$WI = W$。

逆矩阵(inverse)$W^{-1}$:如果 $W W^{-1} = I$,则 $W^{-1}$ 是 $W$ 的逆。直觉:$W$ 做了一个变换,$W^{-1}$ 把它"撤销"。注意:不是所有矩阵都有逆——只有当 $W$ 的变换没有"压扁"任何维度(数学上叫满秩、行列式非零)时才可逆。就像把一张纸压成一条线后,你没法恢复原来的纸。

python
import numpy as np

W = np.array([[1.0, 2.0],
              [3.0, 4.0]])

print("转置:\n", W.T)                    # 行列互换

I = np.eye(2)                            # 2x2 单位矩阵
print("W @ I 等于 W 吗:", np.allclose(W @ I, W))   # True(allclose 用于浮点数比较)

W_inv = np.linalg.inv(W)                 # 求逆矩阵
print("W @ W_inv ≈ I 吗:", np.allclose(W @ W_inv, I))  # True

# 奇异矩阵(第二行是第一行的 2 倍,信息被"压扁"了)没有逆
S = np.array([[1.0, 2.0],
              [2.0, 4.0]])
try:
    np.linalg.inv(S)
except np.linalg.LinAlgError as e:
    print("报错:", e)                    # Singular matrix

这个在大模型里用在哪:转置无处不在——注意力机制里算 $QK^T$ 就是转置的典型应用;逆矩阵在大模型里很少直接算(太贵且数值不稳定),但理解"可逆 = 信息无损"对理解为什么网络要设计残差连接很有帮助。

1.4.4 秩(rank):矩阵里"真正独立的信息量"

的直觉定义:一个矩阵的秩,是它包含的独立方向的数量

类比:Excel 表格里,如果第 3 列 = 第 1 列 + 第 2 列,那第 3 列就是"冗余信息",不增加秩。秩度量的是去掉所有冗余后,真正有用的列有几个

python
import numpy as np

A = np.array([[1.0, 2.0, 3.0],
              [4.0, 5.0, 6.0],
              [7.0, 8.0, 10.0]])   # 3 行 3 列,行列互不冗余
print("A 的秩:", np.linalg.matrix_rank(A))   # 3 —— 满秩,信息无损

B = np.array([[1.0, 2.0],
              [2.0, 4.0],
              [3.0, 6.0]])         # 第 2 列永远是第 1 列的 2 倍
print("B 的秩:", np.linalg.matrix_rank(B))   # 1 —— 看似 2 列,实际只有 1 个独立方向

这个在大模型里用在哪这是本章最重要的伏笔! 研究发现,大模型微调时权重矩阵的"变化量"虽然尺寸巨大,但秩很低——也就是说真正发生变化的有效方向很少。既然如此,何必存储整个巨大矩阵?存两个小矩阵就行了。这就是 LoRA 微调(第 5、6 章)的数学根基,1.6 节马上展开。

📝 练习 1.3

  1. 构造一个 $(4, 3)$ 的矩阵 $W$ 和一个 3 维向量 $\mathbf{x}$,计算 $W\mathbf{x}$ 并打印每一步的形状。
  2. 构造一个 3x3 矩阵,其中第 3 列是前两列之和,用 np.linalg.matrix_rank 验证它的秩是 2。

1.5 特征值与特征向量:变换的"主方向"

1.5.1 直觉:被矩阵"优待"的方向

回忆视角一:矩阵乘向量 = 对向量做旋转 + 拉伸。一般向量被 $W$ 乘过之后,方向就变了。

但有些特殊方向例外:矩阵作用在它们身上时,方向不变,只改变长度(可能拉长、缩短或翻转):

$$W\mathbf{v} = \lambda \mathbf{v}$$

  • $\mathbf{v}$ 叫特征向量(eigenvector):被变换"优待"的方向
  • $\lambda$(读作 lambda)叫特征值(eigenvalue):在这个方向上的伸缩倍数

类比:旋转地球时,所有点都在动,但南北极轴线方向上的点不动——地轴就是旋转矩阵的特征向量,特征值为 1。

再看 $|\lambda|$ 最大的那个特征向量:它对应变换中拉伸最厉害的方向,也就是这个矩阵"最看重的主方向"。

python
import numpy as np

W = np.array([[2.0, 1.0],
              [1.0, 2.0]])

# 求特征值与特征向量(eigh 专用于 W 这样的对称矩阵,返回实数特征值,升序排列)
eigenvalues, eigenvectors = np.linalg.eigh(W)
print("特征值:", eigenvalues)          # [1. 3.]
print("特征向量(每列一个):\n", eigenvectors)

# 验证定义:W @ v 应该等于 lambda * v(方向不变,只缩放)
v = eigenvectors[:, 1]                 # 取最大特征值对应的特征向量
lam = eigenvalues[1]
print("W @ v =", W @ v)
print("λ * v =", lam * v)              # 两者相同(方向不变,长度变为 3 倍)

1.5.2 为什么不用死磕计算

手算特征值需要解行列式方程,繁琐且在大模型工程中几乎用不到。你需要带走的只有三条直觉:

  1. 特征向量 = 矩阵变换的"主方向"(PCA 降维就是找数据协方差矩阵的主特征向量)
  2. 特征值的大小 = 该方向被拉伸的倍数,大特征值对应重要方向,小特征值对应次要方向
  3. 沿次要方向的信息可以丢弃而不损失太多——这为下一节的 SVD 低秩近似埋下伏笔

这个在大模型里用在哪:① 分析 Hessian 矩阵的特征值可以判断训练收敛的"地形"是山谷还是平原(优化理论研究);② 更重要的是,它是理解下一节 SVD 的垫脚石——SVD 可看作特征值分解在非方阵上的推广。

📝 练习 1.4:用 np.linalg.eig 分析矩阵 [[1, 0], [0, 3]],回答:两个主方向分别是什么?哪个方向被拉伸得更厉害?(提示:这是一个对角矩阵,特征向量就是坐标轴方向。)


1.6 矩阵分解与低秩近似:LoRA 的数学根基

1.6.1 SVD 直觉:把矩阵拆成"旋转 → 拉伸 → 再旋转"

**奇异值分解(SVD, Singular Value Decomposition)**说:任何矩阵 $W \in \mathbb{R}^{m \times n}$ 都可以拆成三个矩阵的乘积:

$$W = U \Sigma V^T$$

  • $U \in \mathbb{R}^{m \times m}$:一次旋转(输出空间的正交基)
  • $\Sigma$(读作 Sigma):一个"对角矩阵",只有对角线上有数,叫奇异值 $\sigma_1 \geq \sigma_2 \geq \dots \geq 0$,表示沿各主方向的拉伸倍数,且从大到小排列
  • $V^T \in \mathbb{R}^{n \times n}$:一次旋转(输入空间的正交基)

类比:任何一个复杂的线性变换,都可以分解为"先把坐标系转好 → 沿各坐标轴拉伸不同倍数 → 再转到目标方向"三步。奇异值就是每个方向的"重要性评分"。

1.6.2 低秩近似:只保留最重要的方向

关键来了:奇异值从大到小排列,而且实践中衰减极快——前几个奇异值往往占了绝大部分"能量"。那么,只保留前 $r$ 个奇异值,其余置零,就得到 $W$ 的 rank-r 近似

$$W \approx U_r \Sigma_r V_r^T$$

存储量从 $m \times n$ 降为 $r \times (m + n + 1)$。当 $r \ll \min(m, n)$ 时,压缩效果惊人——而近似误差由被丢弃的奇异值决定,通常很小。

实验:对一个 8x8 矩阵做 rank-2 近似,看误差有多大:

python
import numpy as np

np.random.seed(42)

# ---- 构造一个"本质上是低秩"的 8x8 矩阵:W = A @ B,其中 A: (8,2), B: (2,8) ----
# 这样的 W 虽然尺寸是 8x8,但真正独立的方向只有 2 个
A = np.random.randn(8, 2)
B = np.random.randn(2, 8)
W = A @ B
print("W 的形状:", W.shape)                      # (8, 8)
print("W 的秩:", np.linalg.matrix_rank(W))        # 2 —— 64 个数里只有 2 个独立方向!

# ---- 对 W 做 SVD 分解 ----
U, sigma, Vt = np.linalg.svd(W)
print("奇异值:", np.round(sigma, 3))
# 你会看到:前 2 个奇异值很大,后面 6 个几乎为 0

# ---- 只保留前 r=2 个奇异值,重建矩阵 ----
r = 2
W_approx = U[:, :r] @ np.diag(sigma[:r]) @ Vt[:r, :]

# ---- 对比误差:用 Frobenius 范数(所有元素差的平方和开根号)度量 ----
error = np.linalg.norm(W - W_approx)
relative_error = error / np.linalg.norm(W)
print("绝对误差:", error)                        # 接近 0
print("相对误差:", relative_error)               # 接近 0:几乎没有信息损失!

# ---- 存储量对比 ----
print(f"原始存储: {W.size} 个数")
print(f"低秩存储: {U[:, :r].size + sigma[:r].size + Vt[:r, :].size} 个数")
# 64 -> 34,矩阵越大压缩率越惊人(对 4096x4096 的矩阵,rank-16 只用约 1% 的存储)

运行后你会发现:虽然 $W$ 有 64 个元素,但因为它的秩只有 2,用 rank-2 近似重建误差几乎为零——64 个数字里的"有效信息"本来就很少。

1.6.3 伏笔:这就是 LoRA 的数学根基

现在揭示伏笔。大模型微调时,我们需要更新权重矩阵 $W$,记变化量为 $\Delta W$(和 $W$ 一样大)。研究发现:微调引起的 $\Delta W$ 秩很低——真正变化的有效方向很少。

于是 LoRA(Low-Rank Adaptation,低秩适配)的想法是:不直接学 $\Delta W$,而是学两个小矩阵 $A \in \mathbb{R}^{r \times n}$ 和 $B \in \mathbb{R}^{m \times r}$,令 $\Delta W \approx BA$(这正是上面对 W = A @ B 的构造方式):

$$\mathbf{y} = (W + \Delta W)\mathbf{x} = W\mathbf{x} + BA\mathbf{x}$$

效果:对一个 $4096 \times 4096$ 的层,取 $r=16$,可训练参数从 1677 万降到 $2 \times 4096 \times 16 \approx 13$ 万——不到原来的 1%,但微调效果几乎不打折。这就是你刚才亲手验证的低秩近似的威力。

这个在大模型里用在哪:第 5、6 章我们会用 LoRA/QLoRA 真正微调一个模型。到那时请记住今天这个 8x8 的小实验——万变不离其宗。

📝 练习 1.5:修改上面的实验:构造 $W = A @ B + 0.1 \times \text{noise}$(加一个小的随机噪声矩阵模拟真实场景),观察奇异值是否还衰减很快?rank-2 近似的相对误差变成多少?(这模拟了"近似低秩"的真实世界。)

💡 经验法则:看一个矩阵是否"本质低秩",最快的办法是做 SVD 然后画出奇异值曲线。如果曲线断崖式下跌,低秩近似就很有效。大模型很多权重矩阵都呈现这种模式。


1.7 为什么 GPU 擅长矩阵乘法

1.7.1 并行性直觉:一亿次乘法,互不相干

回顾矩阵乘法:$C = AB$ 的每个元素 $c_{ij}$ = $A$ 的第 $i$ 行与 $B$ 的第 $j$ 列的点积。一个 $1024 \times 1024$ 矩阵乘另一个同尺寸矩阵,需要算 $1024^2 \approx 100$ 万个点积,共约 20 亿次乘加运算。

关键在于:这 100 万个点积互相之间没有任何依赖——算 $c_{11}$ 不需要等 $c_{12}$ 算完。这种"天然并行"的结构简直是为 GPU 量身定制:

  • CPU:8~64 个强大核心,一次认真算一个点积
  • GPU:上万个简单核心,一万个点积同时开算

类比:抄写一篇万字文章,CPU 是一个书法家用一小时写完,GPU 是一万个学生每人抄一个字、一秒收工。

1.7.2 算术强度:计算量与访存量之比(萌芽概念)

GPU 算得快,但数据要从显存搬过来。判断一个运算"划不划算",要看算术强度(arithmetic intensity)

$$\text{算术强度} = \frac{\text{计算量(FLOPs)}}{\text{访存量(Bytes)}}$$

对比两个运算(以 $n \times n$ 矩阵为例):

运算计算量访存量算术强度谁瓶颈
矩阵加法 $C = A + B$$n^2$ 次$3n^2$ 个数≈ 0.3访存瓶颈(算得少搬得多)
矩阵乘法 $C = AB$$2n^3$ 次$3n^2$ 个数≈ $n/6$$n$ 大时计算瓶颈

矩阵乘法的计算量随 $n^3$ 增长,访存量只随 $n^2$ 增长——矩阵越大,"每搬一个字节能做的计算"越多,GPU 越容易跑满。这就是为什么深度学习偏爱把计算组织成大矩阵乘法:不是矩阵乘法本身神奇,而是它对硬件最友好

反过来,大模型推理时的逐 token 生成,本质上是"矩阵乘向量"(算术强度低),所以常常 GPU 算力利用率只有 10~20%——大部分时间在等显存搬权重。这正是第 0 章 nvidia-smi 里 GPU-Util 偏低的原因,也是第四阶段(底层优化)KV Cache、批量推理等优化的出发点。

python
import numpy as np
import time

n = 1024
A = np.random.randn(n, n)
B = np.random.randn(n, n)

# 矩阵加法:n^2 次计算,3n^2 次访存 → 算术强度 ≈ 1/3
t0 = time.time()
C1 = A + B
print(f"矩阵加法耗时: {time.time() - t0:.4f} 秒")

# 矩阵乘法:2n^3 次计算,3n^2 次访存 → 算术强度 ≈ n/6 ≈ 170
t0 = time.time()
C2 = A @ B
print(f"矩阵乘法耗时: {time.time() - t0:.4f} 秒")
print(f"乘法计算量是加法的 {2 * n} 倍,耗时却只差几倍——矩阵乘法把算力用满了")

这个在大模型里用在哪:算术强度是理解一切推理优化(KV Cache、continuous batching、量化、FlashAttention)的钥匙。第四阶段我们会回来精确计算它,现在先建立"计算 vs 搬运"这对矛盾的直觉。

📝 练习 1.6:把上面实验的 n 分别改成 128、512、2048,记录矩阵乘法的耗时。计算量增长 $8$ 倍(n 翻倍,$n^3$ 变 8 倍)时,耗时增长多少倍?想想为什么。


1.8 综合实战:纯 NumPy 实现线性层 y = softmax(Wx + b)

收官之战:把本章所有知识串起来,手写一个神经网络线性层(Linear Layer),并用 softmax 把输出变成概率分布——这正是大模型输出层预测下一个 token 的完整数学过程。

softmax 是什么:它把任意一组实数变成"总和为 1 的概率分布":

$$\text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j} e^{z_j}}$$

直觉:$e^x$ 让每个数变正且放大差距("强者愈强"),再除以总和归一化。输入中最大的数,输出概率也最大。

python
import numpy as np

np.random.seed(42)

def softmax(z):
    """把一组实数变成概率分布(含数值稳定技巧)"""
    # 数值稳定:先减去最大值再取 exp,防止 e^z 溢出(e^1000 会爆炸,e^0=1 很安全)
    z = z - np.max(z)
    exp_z = np.exp(z)
    return exp_z / np.sum(exp_z)

# ---- 场景设定:输入 4 维向量,分成 3 类(比如判断一句话的情感:正面/中性/负面)----
n_in, n_out = 4, 3

# 权重矩阵 W:(3, 4) —— 3 个输出类别,每个有 4 维"探测器"
W = np.random.randn(n_out, n_in) * 0.5
print(f"W 形状: {W.shape}")

# 偏置 b:(3,) —— 每个类别的基础分
b = np.random.randn(n_out)
print(f"b 形状: {b.shape}")

# 随机输入 x:(4,) —— 想象它是一个词的 4 维 embedding
x = np.random.randn(n_in)
print(f"x 形状: {x.shape}")
print("-" * 40)

# ---- 前向计算,逐步打印形状 ----
# 第一步:线性变换 Wx,(3,4) @ (4,) -> (3,)
Wx = W @ x
print(f"Wx 形状: {Wx.shape}  值: {np.round(Wx, 3)}")

# 第二步:加偏置,(3,) + (3,) -> (3,)
z = Wx + b
print(f"z = Wx + b 形状: {z.shape}  值: {np.round(z, 3)}")

# 第三步:softmax 变成概率,(3,) -> (3,)
y = softmax(z)
print(f"y = softmax(z) 形状: {y.shape}  值: {np.round(y, 3)}")

print("-" * 40)
print(f"概率之和: {y.sum():.6f}")              # 一定等于 1.0
print(f"预测类别: 第 {np.argmax(y)} 类")        # 取概率最大的类别

一次典型输出:

W 形状: (3, 4)
b 形状: (3,)
x 形状: (4,)
----------------------------------------
Wx 形状: (3,)  值: [-0.659  0.084 -0.004]
z = Wx + b 形状: (3,)  值: [-0.417 -1.829 -1.729]
y = softmax(z) 形状: (3,)  值: [0.661  0.161  0.178]
----------------------------------------
概率之和: 1.000000
预测类别: 第 0 类

恭喜! 你刚刚手写的,就是大模型每一层的核心计算。真实的 GPT 只是把这里的"3 个类别"换成"几万个 token 的词表"、把一层换成几十层,再加上注意力机制(第 4 章)——但矩阵乘向量、加偏置、softmax 出概率这个骨架,和你写的完全一样。

📝 练习 1.7

  1. 把输入从单个向量改成一批 5 个向量 $X \in \mathbb{R}^{4 \times 5}$(用 1.4.1 的视角三),修改代码让 softmax 对每一列分别计算,输出形状应为 $(3, 5)$。
  2. 去掉 softmax 里的"减最大值"技巧,把 W 的系数 0.5 改成 100,观察会发生什么(溢出警告/NaN),体会数值稳定的重要性。

本章小结

知识点一句话总结
标量/向量/矩阵/张量0~N 维的数字容器;embedding 就是每词一个向量
点积对应相乘再求和,度量"方向配合度";注意力分数的本质
范数向量的长度:L2 是直线距离,L1 是出租车距离
余弦相似度点积除以长度,只看方向;RAG/语义检索的数学核心
矩阵乘法三视角线性变换 / 行点积列 / 批量向量变换
$y = Wx + b$神经网络每一层:W 是可学习的权重矩阵
矩阵中真正独立的方向数;低秩 = 有效信息少
特征值/特征向量变换的主方向与拉伸倍数,直觉理解即可
SVD 与低秩近似$W = U\Sigma V^T$,保留大奇异值 → LoRA 的数学根基
算术强度计算量/访存量;矩阵乘法对 GPU 最友好
softmax把任意实数变成概率分布,大模型输出层的最后一步

本章实战验收

  • [ ] 能用一句话向朋友解释"为什么 embedding 是向量",并说出余弦相似度的几何含义
  • [ ] 跑通 1.3.5 的语义检索例子,理解"归一化后点积 = 余弦相似度"
  • [ ] 不看答案写出 $W\mathbf{x}$ 的形状推导规则 $(m,n)@(n,k) \to (m,k)$
  • [ ] 跑通 1.6.2 的 SVD 低秩重建实验,观察到 rank-2 近似误差接近零
  • [ ] 能向朋友解释 LoRA 为什么能把可训练参数压缩 99%
  • [ ] 独立完成 1.8 的 y = softmax(Wx + b) 实战,打印每一步形状
  • [ ] 说清"算术强度"是计算量和什么量的比值,以及矩阵乘法为什么 GPU 友好

拓展阅读

  • 3Blue1Brown《线性代数的本质》(B 站/YouTube 免费视频系列)——本章几何直觉的最佳视觉化教材,强烈建议配套观看
  • 《深度学习》(花书)第 2 章——更严谨的线性代数复习
  • LoRA 原论文LoRA: Low-Rank Adaptation of Large Language Models (Hu et al., 2021)——读 Abstract 和 Figure 1 即可,第 5 章我们会实现它
  • Gil Strang《Introduction to Linear Algebra》——想系统补课的经典教材