跳转至

# 线性变换

每个矩阵乘法都是一个线性变换——一个在保持线性性质的同时重塑、旋转或投影向量的函数。本文涵盖旋转、反射、缩放、剪切、投影、映射的核与像，以及神经网络层如何串联这些变换。

- 线性变换（或线性映射）是一个接收向量并产生另一个向量的函数，同时保持加法和缩放性质。如果 \(T\) 是线性的，则：

  - \(T(\mathbf{u} + \mathbf{v}) = T(\mathbf{u}) + T(\mathbf{v})\)
  - \(T(c\mathbf{u}) = cT(\mathbf{u})\)

- 每个线性变换都可以表示为矩阵乘法。矩阵就是变换本身。当你用一个矩阵乘以一个向量时，就是在对它施加一个线性变换。

- 可以把一个 \(2 \times 2\) 矩阵想象成一个机器：它接收二维向量，输出新的二维向量。矩阵的列告诉你标准基向量 \(\hat{\mathbf{i}}\) 和 \(\hat{\mathbf{j}}\) 经过变换后到了哪里。其余一切都由线性性质导出。

- 例如，如果

\[ A = \begin{bmatrix} 2 & 1 \\ 1 & 2 \end{bmatrix} \]

那么 \(\hat{\mathbf{i}} = [1, 0]^T\) 落在 \([2, 1]^T\)（第1列），\(\hat{\mathbf{j}} = [0, 1]^T\) 落在 \([1, 2]^T\)（第2列）。其他所有向量都是这两个向量的组合，因此其输出自动遵循。

- 将两个矩阵相乘可以理解为依次施加两个变换。如果 \(B\) 将向量从一个空间变换，然后 \(A\) 变换结果，那么 \(AB\) 按顺序完成这两个操作。在游戏引擎中，先旋转角色再向前移动，与先移动再旋转，结果完全不同——这就是矩阵乘法不满足交换律的原因。

- 旋转将向量绕一定角度 \(\theta\) 转动而不改变其长度。向量大小不变，只是指向新的方向。

- 二维中的旋转矩阵为：

\[ R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} \]

- 当 \(\theta = 90°\) 时：

\[ R = \begin{bmatrix} 0 & -1 \\ 1 & 0 \end{bmatrix} \]

因此 \([1, 0]^T\) 变成 \([0, 1]^T\)。原来指向右侧的向量现在指向上方。旋转矩阵是正交的，且行列式始终为1。当你在手机上旋转照片时，就是对每个像素坐标应用这个矩阵。

- 在三维中，每个坐标轴都有对应的旋转矩阵。机械臂的每个关节绕特定轴旋转，每个关节就是一个旋转矩阵。绕z轴旋转看起来像是嵌入三维的二维情况：

\[ R_z(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \]

- 缩放沿每个坐标轴独立地拉伸或压缩向量：

\[ S(s_x, s_y) = \begin{bmatrix} s_x & 0 \\ 0 & s_y \end{bmatrix} \]

- \(S(2, 1.5)\) 将x分量加倍，y分量乘以1.5。沿某轴缩放 \(-1\) 会翻转该分量。对角矩阵总是缩放变换。当你将图片缩小到50%时，就是对每个像素坐标应用 \(S(0.5, 0.5)\)。

- 反射像镜子一样将向量翻转到某个轴或直线的另一侧。沿x轴的反射保持x分量不变，取反y分量：

\[ \text{Ref}_x = \begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix} \]

- 例如，\([3, 2]^T\) 变成 \([3, -2]^T\)。当你的手机水平翻转自拍照使文字正确显示时，就是在应用反射矩阵。沿直线 \(y = x\) 的反射交换两个分量：

\[ \text{Ref}_{y=x} = \begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix} \]

- 反射矩阵的行列式为 \(-1\)，表明它们翻转了方向。

- 旋转和反射都是刚性变换：它们保持距离和角度不变。表示这些变换的矩阵是正交矩阵，这就是为什么正交矩阵的行列式总是 \(+1\)（旋转）或 \(-1\)（反射）。

- 剪切沿一个坐标轴按另一坐标轴的比例倾斜向量。水平剪切因子 \(k\)：

\[ \text{Sh}_x(k) = \begin{bmatrix} 1 & k \\ 0 & 1 \end{bmatrix} \]

- 每个点水平滑动 \(k\) 倍于其高度的距离。当 \(k = 0.5\) 时，高度为2的点向右移动1。最下面一行保持不动，最上面一行滑动最多。这就是斜体文字的工作原理：正立的字母被剪切，从而向右倾斜。

- 以上所有变换（旋转、缩放、反射、剪切）都是线性变换。它们保持原点固定，并保持直线为直线。但平移（将所有点按固定量移动）呢？

- 平移不是线性变换，因为它移动了原点。如果将每个点向右移动3，零向量会移动到 \([3, 0]^T\)，从而破坏了线性性质。为了处理平移，我们使用仿射变换，它将线性变换与平移结合起来：

\[\mathbf{y} = A\mathbf{x} + \mathbf{t}\]

- 为了将其表示为单个矩阵乘法，我们使用齐次坐标：为每个向量添加一个额外的1，并使用一个 \((n+1) \times (n+1)\) 的矩阵：

\[ \begin{bmatrix} A & \mathbf{t} \\ \mathbf{0}^T & 1 \end{bmatrix} \begin{bmatrix} \mathbf{x} \\ 1 \end{bmatrix} = \begin{bmatrix} A\mathbf{x} + \mathbf{t} \\ 1 \end{bmatrix} \]

- 仿射变换保持直线和平行性，但不一定保持角度或长度。电子游戏中的每个物体都使用仿射变换来定位：旋转它、缩放它，然后放置到正确的位置——所有这些都编码在一个矩阵中。

- 退化变换（奇异矩阵）将空间坍缩到更低维度。

- 例如，矩阵

\[ \begin{bmatrix} 1 & 2 \\ 2 & 4 \end{bmatrix} \]

将每个二维向量映射到一条直线上，因为两列指向同一方向。行列式为零，信息丢失，且该变换不可逆。

- 将彩色图像（每个像素有3个值：红、绿、蓝）转换为灰度图（每个像素1个值）就是退化变换：颜色信息永久丢失。

- 在机器学习中，线性变换是神经网络的核心。数据被表示为矩阵（向量的堆叠，这些向量代表对象的特征——人、飞机、文本、图像……任何东西！）

- 每一层应用一个矩阵乘法（线性变换），详细内容在其他章节中提供，我们需要解释如何组织这些数据并恰当地引出神经网络。

- 然而，当今最常用的技术几乎完全是将数据通过一系列线性变换传递，我们称之为Transformer。

- Gemini、ChatGPT、Claude、Qwen、DeepSeek以及当今世界上性能最好的AI，都是Transformer！

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

1. 对向量应用旋转矩阵，并绘制原始向量和旋转后的向量。尝试不同的角度。

    import jax.numpy as jnp
    import matplotlib.pyplot as plt

    theta = jnp.pi / 3
    R = jnp.array([[jnp.cos(theta), -jnp.sin(theta)],
                   [jnp.sin(theta),  jnp.cos(theta)]])

    v = jnp.array([1.0, 0.0])
    v_rot = R @ v

    plt.figure(figsize=(5, 5))
    plt.quiver(0, 0, v[0], v[1], angles='xy', scale_units='xy', scale=1, color='red', label='original')
    plt.quiver(0, 0, v_rot[0], v_rot[1], angles='xy', scale_units='xy', scale=1, color='blue', label='rotated')
    plt.xlim(-1.5, 1.5); plt.ylim(-1.5, 1.5)
    plt.grid(True); plt.legend(); plt.gca().set_aspect('equal')
    plt.show()

1. 对构成正方形的一组点应用剪切变换，并可视化变形后的形状。

    import jax.numpy as jnp
    import matplotlib.pyplot as plt

    square = jnp.array([[0,0],[1,0],[1,1],[0,1],[0,0]]).T

    k = 0.5
    shear = jnp.array([[1, k],
                        [0, 1]])
    sheared = shear @ square

    plt.figure(figsize=(6, 4))
    plt.plot(square[0], square[1], 'r-o', label='original')
    plt.plot(sheared[0], sheared[1], 'b-o', label='sheared')
    plt.grid(True); plt.legend(); plt.gca().set_aspect('equal')
    plt.show()