跳转至

# 矩阵运算

矩阵运算是深度学习的计算引擎。本文涵盖矩阵加法、标量乘法、矩阵-向量积、矩阵乘法、逐元素运算、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} \]

- 矩阵能做的最简单的事情是乘以一个向量。矩阵-向量乘法 \(A\mathbf{x}\) 使用 \(\mathbf{x}\) 的分量作为权重来组合 \(A\) 的列：

\[ \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} \]

- 这是机器学习中的核心运算。每个神经网络层都计算 \(A\mathbf{x} + \mathbf{b}\)：矩阵乘以输入向量，再加上偏置。

- 一般情况是矩阵乘法。给定 \(A\)（\(m \times n\)）和 \(B\)（\(n \times p\)），乘积 \(C = AB\) 是一个 \(m \times p\) 矩阵，每个元素都是一个点积：

\[C_{ij} = \sum_{k=1}^{n} A_{ik} B_{kj}\]

- 结果中的每个条目都是 \(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积（逐元素乘积）将两个相同大小的矩阵逐项相乘，记作 \(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\)），且要求两个矩阵维度相同。它在机器学习中广泛用于门控机制：通过与一个取值在0到1之间的掩码逐元素相乘，控制每个条目"通过"多少。

- 两个向量 \(\mathbf{u}\) 和 \(\mathbf{v}\) 的外积产生一个矩阵：\(\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\) 的矩阵，那就是十亿次乘法。

- 当矩阵是稀疏的（大部分为零）时，朴素的乘法会浪费时间乘以零。压缩稀疏行（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]。这跳过了所有零，使稀疏运算快得多。

- 矩阵的一个核心用途是求解线性方程组。方程组 \(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} \]

- 矩阵逐行乘以向量（每一行与 \([x_1, x_2]^T\) 点积）得到两个方程：

\[2x_1 + 1x_2 = 5 \qquad \text{(第1行)} \qquad \qquad x_1 + 3x_2 = 10 \qquad \text{(第2行)}\]

- 从第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}\)。但直接计算逆矩阵代价高昂且数值不稳定。实践中我们使用分解方法。

- 并非所有矩阵都是方阵，也不是所有方阵都可逆。伪逆 \(A^+\) 将逆推广到任意矩阵。它总是存在，并提供"尽可能好的"逆：

\[A^+ = (A^TA)^{-1}A^T\]

- 当 \(A\) 是下三角矩阵时，通过前向代入求解 \(L\mathbf{x} = \mathbf{b}\) 很容易：先解出 \(x_1\)，然后用它求出 \(x_2\)，依此类推。

- 当 \(A\) 是上三角矩阵时，通过回代求解 \(U\mathbf{x} = \mathbf{b}\)：先解出最后一个变量，然后向上求解。

- 这就是为什么将矩阵分解为三角因子（如分解章节所述）如此有用——它将一个难题转化为两个简单问题。

## 编程练习（使用CoLab或Jupyter Notebook）

1. 将两个矩阵相乘并验证维度。然后交换顺序，观察结果如何变化（或者，如果维度不匹配，运算失败）。

    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)}")

1. 求解线性方程组 \(A\mathbf{x} = \mathbf{b}\)，并通过回代乘法验证解。尝试改变 \(\mathbf{b}\)，观察解如何变化。

    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}")