矩阵运算 矩阵运算是深度学习的计算引擎。本文件讲解矩阵加法、标量乘法、矩阵-向量积、矩阵乘法、逐元素运算、Kronecker 积与广播——每一次前向传播和梯度更新背后的运算。 矩阵可以像向量一样相加和数乘。 加法要求两个矩阵维度相同,逐元素相加: 标量乘法就是把每个元素都乘以这个标量: 矩阵能做的最简单的事,就是乘一个向量。矩阵-向量乘法(matrix-vector multiplication) $A\mathbf{x}$ 是以 $\mathbf{x}$ 的分量为权重,把 $A$ 的各列组合起来: 这是 ML 中最核心的运算。每一个神经网络层都在计算 $A\mathbf{x} + \mathbf{b}$:一个矩阵乘以输入向量,再加上一个偏置。
矩阵运算是深度学习的计算引擎。本文件讲解矩阵加法、标量乘法、矩阵-向量积、矩阵乘法、逐元素运算、Kronecker 积与广播——每一次前向传播和梯度更新背后的运算。
矩阵可以像向量一样相加和数乘。
加法要求两个矩阵维度相同,逐元素相加:
\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} + \begin{bmatrix} 5 & 6 \\ 7 & 8 \end{bmatrix} = \begin{bmatrix} 6 & 8 \\ 10 & 12 \end{bmatrix}
3 \times \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} = \begin{bmatrix} 3 & 6 \\ 9 & 12 \end{bmatrix}
\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \begin{bmatrix} 5 \\ 6 \end{bmatrix} = 5 \begin{bmatrix} 1 \\ 3 \end{bmatrix} + 6 \begin{bmatrix} 2 \\ 4 \end{bmatrix} = \begin{bmatrix} 17 \\ 39 \end{bmatrix}
这是 ML 中最核心的运算。每一个神经网络层都在计算 A\mathbf{x} + \mathbf{b}:一个矩阵乘以输入向量,再加上一个偏置。
更一般的情况是矩阵乘法(matrix multiplication)。给定 A(m \times n)和 B(n \times p),乘积 C = AB 是一个 m \times p 的矩阵,其中每个元素都是一个点积:
结果中的每一项是 A 的一行与 B 的一列的点积。内层维度必须匹配(n),结果取外层维度(m \times p)。
另一种理解方式:结果的每一列是 A 各列的一个加权和,权重来自 B 中对应的那一列。
如果 B 的某一列是 [2, 3]^T,那么结果的那一列就是 2 \times (\text{A 的第 1 列}) + 3 \times (\text{A 的第 2 列})。
一个有用的特例:矩阵乘它自己的转置,结果总是方阵。AA^T 是 m \times m,A^TA 是 n \times n:
\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix} \begin{bmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{bmatrix} = \begin{bmatrix} 14 & 32 \\ 32 & 77 \end{bmatrix}
矩阵乘法有几条重要的规则:
不满足交换律:一般情况下 AB \neq BA,顺序很重要。
满足结合律:(AB)C = A(BC)。你可以随意地分组。
满足分配律:A(B + C) = AB + AC。
单位元:AI = IA = A。
Hadamard 积(Hadamard product)(逐元素乘积)把两个同样大小的矩阵逐项相乘,记作 A \odot B:
\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} \odot \begin{bmatrix} 5 & 6 \\ 7 & 8 \end{bmatrix} = \begin{bmatrix} 5 & 12 \\ 21 & 32 \end{bmatrix}
与普通的矩阵乘法不同,Hadamard 积满足交换律(A \odot B = B \odot A),并且要求两个矩阵维度相同。它在 ML 里被大量用于门控(gating):逐元素地乘上一个值在 0 到 1 之间的掩码,就能控制每个分量「放行」多少。
两个向量 \mathbf{u} 和 \mathbf{v} 的**外积(outer product)**产生一个矩阵:\mathbf{u}\mathbf{v}^T。每一项是 \mathbf{u} 的一个元素与 \mathbf{v} 的一个元素的乘积:
\begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} \begin{bmatrix} 4 & 5 \end{bmatrix} = \begin{bmatrix} 4 & 5 \\ 8 & 10 \\ 12 & 15 \end{bmatrix}
结果的秩总是 1,因为每一行都是 \mathbf{v}^T 的缩放版本。任意矩阵都可以写成若干个秩为 1 的外积之和,这正是 SVD 所做的事(在「分解」一节中讲解)。
矩阵乘法的计算代价很高。两个 n \times n 矩阵相乘需要 O(n^3) 次运算。对于一个 1000 \times 1000 的矩阵,那就是十亿次乘法。
当矩阵是稀疏的(大部分为零)时,朴素的乘法会把大量时间浪费在与零相乘上。**压缩稀疏行(Compressed Sparse Row,CSR)**格式只存储非零元素及其位置:
例如,下面这个矩阵:
A = \begin{bmatrix} 5 & 0 & 0 & 2 \\ 0 & 0 & 3 & 0 \\ 0 & 0 & 0 & -1 \end{bmatrix}
存储为:values = [5, 2, 3, -1],columns = [0, 3, 2, 3],row offsets = [0, 2, 3, 4]。这样就跳过了所有的零,让稀疏运算快得多。
矩阵的一个核心用途是求解线性方程组(systems of linear equations)。方程组 A\mathbf{x} = \mathbf{b} 问的是:「什么向量 \mathbf{x} 被 A 变换后会得到 \mathbf{b}?」
例如,假设你在买水果。苹果每个 x_1 美元,香蕉每个 x_2 美元。你知道 2 个苹果加 1 个香蕉要 5 美元,1 个苹果加 3 个香蕉要 10 美元。写成矩阵形式:
\begin{bmatrix} 2 & 1 \\ 1 & 3 \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} 5 \\ 10 \end{bmatrix}
由第 1 行得 x_2 = 5 - 2x_1。代入第 2 行:x_1 + 3(5 - 2x_1) = 10,解得 x_1 = 1,于是 x_2 = 3。苹果每个 1 美元,香蕉每个 3 美元。
验证一下——结果正确:
\begin{bmatrix} 2 & 1 \\ 1 & 3 \end{bmatrix} \begin{bmatrix} 1 \\ 3 \end{bmatrix} = \begin{bmatrix} 2 + 3 \\ 1 + 9 \end{bmatrix} = \begin{bmatrix} 5 \\ 10 \end{bmatrix}
如果 A 有逆,解就是 \mathbf{x} = A^{-1}\mathbf{b}。但直接求逆既昂贵又在数值上不稳定。实际中我们会改用分解方法。
并非每个矩阵都是方阵,也并非每个方阵都可逆。伪逆(pseudo-inverse) A^+ 把逆推广到了任意矩阵。它永远存在,并提供「尽可能好」的逆:
当 A 是下三角矩阵时,求解 L\mathbf{x} = \mathbf{b} 用**前代换(forward substitution)**很容易:先解出 x_1,再用它求 x_2,如此向下进行。
当 A 是上三角矩阵时,求解 U\mathbf{x} = \mathbf{b} 用回代(back substitution):先解最后一个变量,再向上推进。
这正是把矩阵分解为三角因子(见「分解」一节)如此有用的原因。它把一个难题变成两个简单的题。
import jax.numpy as jnp A = jnp.array([[1.0, 2.0], [3.0, 4.0]]) B = jnp.array([[5.0, 6.0], [7.0, 8.0]]) print(f"A @ B:\n{A @ B}") print(f"B @ A:\n{B @ A}") print(f"Equal: {jnp.allclose(A @ B, B @ A)}")
import jax.numpy as jnp A = jnp.array([[2.0, 1.0], [5.0, 3.0]]) b = jnp.array([4.0, 7.0]) x = jnp.linalg.solve(A, b) print(f"Solution x: {x}") print(f"A @ x: {A @ x}")