线性代数

向量与向量空间

  • 向量与向量空间
    • 什么是向量
      • 向量是一个有方向的量
      • 几何视角:空间中从原点出发的一支箭头,方向和长度都有意义
      • 代数视角:一组有序的数字
      • 示例
        • 这是一个三维向量,每个数字对应一个坐标轴上的分量。
      • 词嵌入
        • 一个词(token)会被映射成一个几百甚至几千维的向量
        • 比如 “猫” 这个词可能被表示成一个 768 维的向量——你可以把它想象成 768 个坐标轴上的一个点
        • 语义相近的词在这个空间里位置接近
    • 向量的基本运算
      • 向量加法
        • 两个向量相加,对应分量分别相加
        • 几何意义: 把 b⃗\vec{b} 的起点接到 a⃗\vec{a} 的终点,结果是合向量。
        • 在 LLM 里的例子:有研究发现,词向量之间存在近似的语义关系
        • vec(“国王”)−vec(“男人”)+vec(“女人”)≈vec(“女王”)
      • 标量乘法
        • 用一个数(标量)乘以向量,每个分量都乘以这个数
        • 几何意义:把向量拉伸或压缩
    • 点积
      • 定义
        • 两个同维度向量的点积
      • 示例
      • 几何意义
        • 点积有另一种等价写法
        • 其中 θ\theta 是两向量之间的夹角
      • 关键洞察
        • 点积可以衡量两个向量"有多相似"。
        • 这正是 Attention 机制里 Query 和 Key 做点积的本质——算两个向量的相关程度
    • 向量范数(Norm)
      • 范数衡量向量的"长度"或"大小"
      • L2 范数(最常用)
        • 就是我们熟悉的欧几里得距离
      • L1 范数
        • 各分量绝对值之和,在正则化(防止过拟合)中常用。
    • 余弦相似度
      • 把点积和范数组合起来,就得到余弦相似度——LLM 里衡量语义相似性的核心工具
      • 余弦相似度只关心方向,不关心长度
      • 两个词的向量可能长度不同,但如果方向相同,说明它们在语义上是一致的
    • 向量空间
      • 满足以下条件的集合叫向量空间
        • 元素(向量)之间可以相加,可以被标量乘
        • 在这个集合里做加法和乘法,结果还在这个集合里
      • 最关键的概念是维度(Dimension):向量空间需要多少个基向量来描述其中所有的点
      • GPT-2 用 768 维的向量空间来表示词义,GPT-3 用 12288 维。维度越高,理论上能编码的信息越丰富
    • Numpy 代码

      import numpy as np
      
      # ─── 1. 创建向量 ───────────────────────────────────────
      a = np.array([1, 2, 3], dtype=float)
      b = np.array([4, -1, 2], dtype=float)
      
      print("向量 a:", a)
      print("向量 b:", b)
      
      # ─── 2. 向量加法与标量乘法 ─────────────────────────────
      print("\n向量加法 a + b:", a + b)
      print("标量乘法 2 * a:", 2 * a)
      
      # ─── 3. 点积 ──────────────────────────────────────────
      dot_product = np.dot(a, b)
      print("\n点积 a · b:", dot_product)
      
      # 手动验证
      manual_dot = sum(a[i] * b[i] for i in range(len(a)))
      print("手动计算点积:", manual_dot)
      
      # ─── 4. L2 范数 ────────────────────────────────────────
      norm_a = np.linalg.norm(a)
      norm_b = np.linalg.norm(b)
      print("\n‖a‖₂ =", norm_a)
      print("‖b‖₂ =", norm_b)
      
      # ─── 5. 余弦相似度 ─────────────────────────────────────
      cos_sim = dot_product / (norm_a * norm_b)
      print("\n余弦相似度(a, b):", cos_sim)
      
      # ─── 6. 模拟词嵌入:哪个词和"猫"最相似? ──────────────
      # 用随机向量模拟(实际中是模型学习出来的)
      np.random.seed(42)
      embedding_dim = 8  # 简化为 8 维演示
      
      word_vectors = {
          "猫":   np.random.randn(embedding_dim),
          "狗":   np.random.randn(embedding_dim),
          "汽车": np.random.randn(embedding_dim),
          "小猫": np.random.randn(embedding_dim),
      }
      
      # 手动让"猫"和"小猫"更相似
      word_vectors["小猫"] = word_vectors["猫"] + np.random.randn(embedding_dim) * 0.3
      
      def cosine_similarity(v1, v2):
          return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
      
      query = word_vectors["猫"]
      print("\n与"猫"的余弦相似度:")
      for word, vec in word_vectors.items():
          if word != "猫":
              sim = cosine_similarity(query, vec)
              print(f"  {word}: {sim:.4f}")
      

矩阵基本运算

  • 矩阵基本运算
    • 矩阵是什么
      • 是数字排列成的二维表格,用行数 x 列数描述它的形状(称为"维度"或"shape")
      • 这是一个 2×3 矩阵(2 行 3 列)。用 AijA_{ij}​ 表示第 i 行第 j 列的元素,比如 A12=2A_{12} = 2
      • 矩阵的本质是线性变换。
      • 描述的是"把一个向量变成另一个向量"的规则。
    • 矩阵与向量的乘法
      • 计算规则
        • 矩阵 A(m×n)A(m \times n) 乘以向量 x⃗\vec{x}(n 维),结果是一个 m 维向量
        • 结果的每一行,就是矩阵那一行与向量做点积。
      • 具体例子
        • 一个 3×2 的矩阵,把一个 2 维向量变成了 3 维向量。
        • 维度变了——这正是"变换"的含义。
      • 几何直觉
        • 该矩阵可以把任意 2D 向量逆时针旋转 90 度
        • 原来朝右的向量,变成了朝上的向量。矩阵就是这样编码变换规则的。
        • 矩阵 A 乘以向量 x⃗\vec{x} 等于把 x⃗\vec{x} 所在的空间拉伸、旋转、投影到另一个空间。
      • LLM 中的应用
        • Transformer 里,每个 token 的向量 x⃗\vec{x} 乘以权重矩阵 WQW_Q​,得到 Query 向量
        • q⃗=WQx⃗\vec{q} = W_Q\vec{x}
        • 这就是一次线性变换——把输入向量投影到"Query 空间"
    • 矩阵乘法
      • 计算规则
        • 两个矩阵相乘:A(m×k)×B(k×n)=C(m×n)A(m\times k) \times B (k\times n) = C(m\times n)
      • 关键约束
        • A 的列数必须等于 B 的行数(都是 k)。
        • 结果矩阵 C 的第 i 行第 j 列
        • 即 A 的第 i 行与 B 的第 j 列做点积。
      • 具体例子
      • 矩阵乘法的本质:变换的复合
        • 单独看计算规则很枯燥。真正重要的是它的意义
        • 矩阵 AB 表示"先做变换 B,再做变换 A"。
        • 就像函数复合 f(g(x))f(g(x)),矩阵乘法是在组合两个线性变换。
        • 神经网络的多层结构,本质上就是多个矩阵变换串联在一起,每一层都对数据做一次变换。
      • 矩阵乘法的重要性质
        • 不满足交换律
          • AB≠BAAB \neq BA(大多数情况下)
          • 先旋转再缩放,和先缩放再旋转,结果不同——顺序很重要。
        • 满足结合律
          • (AB)C=A(BC)(AB)C=A(BC)
          • 这让我们可以灵活选择计算顺序(影响效率,不影响结果)
    • 转置
      • 把矩阵的行和列互换,记作 ATA^T
      • AijT=AjiA_{ij}^T=A_{ji}
      • 转置的重要性质
        • (AB)T=BTAT(AB)^T=B^TA^T
        • (a⃗Tb⃗)=a⃗⋅b⃗(\vec{a}^T\vec{b}) = \vec{a}\cdot \vec{b}
        • 转置把向量点积统一进了矩阵符号
      • LLM 的直接应用
        • Attention 公式里的 QKTQK^T 就是对 K 做转置后再和 Q 相乘,目的是让每个 Query 向量和每个 Key 向量都算一次点积,得到注意力分数矩阵。
    • 逆矩阵
      • 对于方阵(行数=列数) A,如果存在矩阵 A−1A^{-1} 使得 AA−1=A−1A=IAA^{-1}=A{-1}A=I
      • 其中 I 是单位矩阵,对角线全是 1,其余是 0,那么 A−1A^{-1} 叫做 A 的逆矩阵
      • 单位矩阵就像数字里的 1,乘以任何矩阵都不改变它:AI=IA=AAI=IA=A
      • 几何直觉:A 把空间做了某种变换,A−1A^{-1} 就是吧这个变换撤销,还原回去
      • 注意:不是所有方阵都有逆矩阵。如果矩阵把空间"压扁"了(比如把三维压成二维),就无法恢复,这样的矩阵不可逆
    • 在 LLM 里,矩阵是怎么用的
      • Transformer 输入是一个序列,比如 4 个 token,每个 token 用 8 维向量表示,整体表示成矩阵 X,形状(4x8)
      • 要生成 Query、Key、Value,分别乘以三个权重矩阵
      • 这三个矩阵乘法,就是在把输入向量投影到三个不同的子空间,分别用于"我在找什么"(Q)、“我有什么”(K)、“我的内容是什么”(V)。
      • 注意力分数:score=QKTscore=QK^T
      • Q 是 4×dk4\times d_k,KTK^T 是 dk×4d_k\times 4,结果是 4×44\times 4 的矩阵
      • 结果是每个 token 和其他每个 token 的相关程度
      • 整条公式 QKTQK^T 的每一个元素,就是一对 Query 和 Key 向量的点积
    • Numpy 实战

      import numpy as np
      
      # ─── 1. 矩阵与向量的乘法 ──────────────────────────────
      A = np.array([[1, 2],
                  [3, 4],
                  [5, 6]])   # 3×2 矩阵
      
      x = np.array([1, -1])   # 2 维向量
      
      result = A @ x           # @ 是矩阵乘法运算符
      print("A @ x =", result)  # 应得 [-1, -1, -1]
      
      # ─── 2. 矩阵乘法 ──────────────────────────────────────
      B = np.array([[1, 2],
                  [3, 4]])
      
      C = np.array([[5, 6],
                  [7, 8]])
      
      print("\nB @ C =\n", B @ C)
      print("C @ B =\n", C @ B)
      print("交换律不成立:", np.allclose(B @ C, C @ B))  # False
      
      # ─── 3. 转置 ──────────────────────────────────────────
      M = np.array([[1, 2, 3],
                  [4, 5, 6]])
      print("\n原矩阵 shape:", M.shape)      # (2, 3)
      print("转置后 shape:", M.T.shape)     # (3, 2)
      print("转置:\n", M.T)
      
      # ─── 4. 模拟 Attention 里的 QK^T ──────────────────────
      np.random.seed(0)
      
      seq_len = 4    # 4 个 token
      d_model = 8   # 每个 token 8 维
      d_k = 4       # Query/Key 的维度
      
      # 输入序列矩阵:4 个 token,每个 8 维
      X = np.random.randn(seq_len, d_model)
      
      # 权重矩阵(实际中是训练得到的)
      W_Q = np.random.randn(d_model, d_k)
      W_K = np.random.randn(d_model, d_k)
      
      # 计算 Q 和 K
      Q = X @ W_Q   # shape: (4, 4)
      K = X @ W_K   # shape: (4, 4)
      
      # 计算注意力分数矩阵
      scores = Q @ K.T   # shape: (4, 4)
      
      print("\nQ shape:", Q.shape)
      print("K^T shape:", K.T.shape)
      print("注意力分数矩阵 shape:", scores.shape)
      print("注意力分数矩阵:\n", scores.round(2))
      
      # 每个元素 scores[i][j] 就是第 i 个 token 对第 j 个 token 的注意力分数
      print("\ntoken 0 对所有 token 的注意力分数:", scores[0].round(2))
      

线性变换的几何直觉

  • 线性变换的几何直觉
    • 核心洞察:矩阵的列=基向量的去向
      • 什么是基向量
        • 二维平面最自然的一组基向量
        • 任何 2D 向量都可以写成基向量的组合
      • 矩阵的读法
        • i^\hat{i} 经过 A 变换后,落在 [21]\begin{bmatrix} 2 \\ 1 \end{bmatrix}
        • j^\hat{j} 经过 A 变换后,落在 [−13]\begin{bmatrix} -1 \\ 3 \end{bmatrix}
        • 线性变换有一个神奇的性质:它保留了向量加法和数乘。
        • 只要你知道基向量被变到哪里,整个空间所有向量的去向就都确定了
        • 这就是矩阵能用一组数字描述整个变换的原因
        • 矩阵×向量,本质是用向量的分量作为系数,对矩阵的列做线性组合
    • 几种常见的线性变换
      • 缩放
        • i^\hat{i} 拉到 [2,0], j^\hat{j} 拉到 [0,2],整个空间被均匀放大 2 倍
        • 不均匀缩放,横向拉伸 3 倍,纵向不变。
      • 旋转
        • 逆时针旋转角度 θ\theta
      • 错切 (Shear)
        • i^\hat{i} 不动,j^\hat{j} 从 [0,1] 移动到 [1,1]。整个空间像被协推的扑克牌
      • 投影 (Projection)
        • i^\hat{i} 不动,j^\hat{j} 压扁到原点,整个 2D 平面被投影到 xx 轴,降了一维
        • 这个变换是不可逆的,一旦把平面压扁成一条线,无法恢复
      • 反射 (Reflection)
        • i^\hat{i} 翻到 [-1,0],整个空间沿 y 轴镜像翻转
    • 行列式:变换的缩放因子
      • 行列式 det(A)det(A) 是一个数
      • 这个变换把单位面积或体积放大缩小了多少倍
      • 几何意义
        • i^\hat{i} 和 j^\hat{j} 围成的单位正方形,面积是 1
        • 变换后,这两个向量会围成一个平行四边形,这个平行四边形的面积就是 ∣det(A)∣|det(A)|
      • 二阶行列式公式
      • det(A) = 0 -> 矩阵不可逆 -> 变换降低了维度
    • 秩 (Rank):变换后的输出维度
      • 秩 (rank(A)) 是另外一个数
      • 回答:变换后,所有向量落到的空间有几维
      • 直观理解
        • [2,4] = 2x[1,2],两列共线,无论 i^\hat{i} 和 j^\hat{j} 落到哪里,它们都在一条直线上
        • 整个 2d 平面被压成了一条 1D 直线,rank(A) = 1
        • 如果两列线性无关(不共线),秩为 2,变换后仍然是 2D 平面,满秩
      • 满秩 vs 不满秩
      • 秩的另一种表述
        • rank(A) = A 的列向量中线性无关的最大个数
        • 也等于行向量中线性无关的最大个数,行秩=列秩
      • 线性相关与无关
        • 一组向量是线性相关的,意味着其中某个向量可以由其他向量的线性组合得到
        • n 个线性无关的向量,才能"撑起"一个 n 维空间
        • 如果你有 100 个向量,但它们都共线(线性相关),实际上只描述了一条 1D 直线
    • 连接到 LLM
      • 为什么 LLM 用高维向量
        • GPT-3 用 12288 维的词向量
        • 维度越高,能容纳的"线性无关方向"越多,理论上能编码越丰富的语义特征
        • 情感、词性、领域、时态、语气……每个特征可以是一个独立的方向。
      • 低秩的危险
        • 如果训练出的某个权重矩阵秩很低,意味着它实际上只在少数几个方向上有效,浪费了表达能力
        • 这是"模型坍缩"的一种数学表现
      • LoRA 的精妙之处
        • LoRA(Low-Rank Adaptation)微调的核心思想:微调时新增的参数变化矩阵 δW\delta W 通常是低秩的
        • 如果 δW\delta W 是 1024x1024 的矩阵,参数量是约 100 万。但如果它的秩只有 8,可以分解成:δW=A⋅B\delta W = A\cdot B
        • 其中 A 是 1024 x 8,B 是 8 x 1024,参数量降到约 1.6 万
      • Attention 的几何意义
        • Q=XWQQ=XW_Q,这是在用矩阵 WQW_Q 把输入向量变换到 Query 空间
        • WQW_Q 的秩决定了 Query 空间的自由度—能从输入里提取多少个独立的"问题"。
    • 可视化代码

      import numpy as np
      import matplotlib.pyplot as plt
      
      def plot_transformation(matrix, title):
          """可视化一个 2x2 矩阵的变换效果"""
          # 原始基向量
          i_hat = np.array([1, 0])
          j_hat = np.array([0, 1])
      
          # 变换后
          i_new = matrix @ i_hat
          j_new = matrix @ j_hat
      
          # 画一个网格点
          x = np.linspace(-2, 2, 9)
          y = np.linspace(-2, 2, 9)
          X, Y = np.meshgrid(x, y)
          points = np.stack([X.ravel(), Y.ravel()])
          transformed = matrix @ points
      
          fig, axes = plt.subplots(1, 2, figsize=(10, 5))
      
          # 原空间
          axes[0].scatter(points[0], points[1], c='lightblue', s=10)
          axes[0].arrow(0, 0, *i_hat, head_width=0.1, color='red', label='i_hat')
          axes[0].arrow(0, 0, *j_hat, head_width=0.1, color='green', label='j_hat')
          axes[0].set_title("变换前")
          axes[0].set_xlim(-3, 3); axes[0].set_ylim(-3, 3)
          axes[0].grid(True); axes[0].axhline(0); axes[0].axvline(0)
          axes[0].set_aspect('equal')
      
          # 变换后空间
          axes[1].scatter(transformed[0], transformed[1], c='lightcoral', s=10)
          axes[1].arrow(0, 0, *i_new, head_width=0.1, color='red')
          axes[1].arrow(0, 0, *j_new, head_width=0.1, color='green')
          axes[1].set_title(f"变换后\ndet={np.linalg.det(matrix):.2f}, "
                          f"rank={np.linalg.matrix_rank(matrix)}")
          axes[1].set_xlim(-5, 5); axes[1].set_ylim(-5, 5)
          axes[1].grid(True); axes[1].axhline(0); axes[1].axvline(0)
          axes[1].set_aspect('equal')
      
          plt.suptitle(title); plt.tight_layout(); plt.show()
      
      # ─── 试试不同的变换 ───────────────────────────────
      # 1. 缩放
      plot_transformation(np.array([[2, 0], [0, 1.5]]), "缩放:x 方向 2 倍,y 方向 1.5 倍")
      
      # 2. 旋转 45 度
      theta = np.pi / 4
      plot_transformation(
          np.array([[np.cos(theta), -np.sin(theta)],
                  [np.sin(theta),  np.cos(theta)]]),
          "旋转 45 度"
      )
      
      # 3. 错切
      plot_transformation(np.array([[1, 1], [0, 1]]), "错切")
      
      # 4. 投影到 x 轴(注意 det=0, rank=1)
      plot_transformation(np.array([[1, 0], [0, 0]]), "投影到 x 轴(降维!)")
      
      # 5. 反射
      plot_transformation(np.array([[-1, 0], [0, 1]]), "沿 y 轴反射")
      

特征值与特征向量

  • 特征值与特征向量
    • 变换中不变的方向
      • 一个矩阵作用于空间,所有向量都被旋转、拉伸、扭曲。绝大多数向量变换后,方向都改变了
      • 但有些特殊的向量——它们经过变换后,方向不变,只是被拉长或缩短
      • 这种"在变换中保持方向"的向量,就叫做特征向量(eigenvector)。它被拉伸的倍数,叫做特征值(eigenvalue)
    • 形式化定义
      • 特征值方程
        • Av⃗=λv⃗A\vec{v}=\lambda\vec{v}
        • 矩阵 A 作用在向量 v⃗\vec{v} 上,结果等于把 v⃗\vec{v} 缩放 \lamda\lamda 倍
        • \lamda\lamda 是特征值(一个标量,可以是正、负、零,甚至复数)
      • 几何含义
        • 关键洞察:特征向量给出的是矩阵变换的"主轴"。沿着这些主轴,复杂的矩阵变换简化成了简单的"拉伸"
      • 怎么求特征值
        • 推导
          • 行列式为 0 意味着矩阵把空间压扁了,存在一些非零向量被压成了零向量——那些被压扁的方向就是特征方向
        • 实际计算
          • 求特征值
          • 求特征向量
    • 特征分解
      • 如果 n×nn\times n 矩阵 A 有 n 个线性无关的特征向量,可以写成 A=PDP−1A=PDP^{-1}
      • 公式解析
        • PP 是把特征向量按列排起来的矩阵
        • DD 是对角矩阵,对角线上是对应的特征值
        • P−1P^{-1} 是 PP 的逆矩阵
      • 几何含义
        • 任何复杂的线性变换 A,都可以分解成三步简单操作:
          • P−1P^{-1}: 把空间转一下,让特征方向对齐到坐标轴
          • DD: 沿坐标轴各方向独立缩放(因为 D 是对角矩阵)
          • PP: 把空间转回来
        • 整个复杂变换,本质上就是沿着特征方向的简单缩放
      • 矩阵的 n 次幂
        • 计算量从 100 次矩阵乘法降到 n 次标量幂运算
        • 这是 PageRank、马尔可夫链稳态分析的核心计算技巧
    • PCA:特征值的经典应用
      • PCA(主成分分析,Principal Component Analysis)是降维最常用的算法。它的数学本质就是特征分解
      • 问题背景
        • 假设你有 1000 个数据点,每个是 768 维向量
        • 可视化、分析都很困难
        • 能不能找到 2 个最重要的"方向",把数据投影到这两个方向上,保留尽可能多的信息
        • PCA 的答案:找数据协方差矩阵的特征向量,按特征值大小排序,取前 k 个
      • 为什么有效
        • 协方差矩阵 C 描述了数据在各维度的"散布情况"
        • 它的特征向量给出了数据散布最大的方向——这些就是"主成分"
        • 特征值越大,说明数据沿那个方向变化越剧烈,包含的信息越多
        • 如果你的 1000 个数据点几乎都集中在一条直线附近
        • 那条直线就是第一主成分(最大特征值对应的特征向量)
        • 用它一个维度就能描述大部分信息
      • PCA 的步骤
        • 把数据矩阵 X 中心化:每一列减去该列均值
        • 计算协方差矩阵:C=1n−1XTXC=\frac 1{n-1}X^TX
        • 对 C 做特征分解:C=PDP−1C=PDP^{-1}
        • 选 D 中最大的 k 个特征值对应的特征向量,组成投影矩阵 W
        • 降维后的数据: Xnew=XWX_{new}=XW
    • 连接到 LLM
      • 词嵌入的可视化
        • GPT 的 768 维词向量没法直接看。
        • 用 PCA 把它降到 2D 或 3D,就能在散点图上观察
        • “国王"“女王"“公主"这些词是否聚在一起?
        • 这是分析模型语义的常用手段。
      • 模型权重分析
        • Transformer 的注意力矩阵分析中,研究者会看注意力权重矩阵的特征值分布
        • 如果特征值过于集中(少数几个特征值很大,其他都很小)
        • 说明模型在过度关注某些方向,可能存在退化现象。
      • RoPE 位置编码的内在结构
        • 旋转位置编码(RoPE)本质上是用一组旋转矩阵作用在 Query 和 Key 上
        • 这些旋转矩阵的特征值(在复数域)正是 eiθe^{i\theta},对应频率的位置信息
      • 模型压缩的根基
        • 特征分解的更一般形式 SVD是各种模型压缩、剪枝、低秩近似的数学基础
    • Numpy 代码实现

      import numpy as np
      import matplotlib.pyplot as plt
      
      # ─── 1. 求特征值和特征向量 ────────────────────────────
      A = np.array([[3, 1],
                  [0, 2]], dtype=float)
      
      eigenvalues, eigenvectors = np.linalg.eig(A)
      print("特征值:", eigenvalues)              # [3., 2.]
      print("特征向量(按列):\n", eigenvectors)
      
      # 验证 A v = λ v
      v0 = eigenvectors[:, 0]    # 第一个特征向量
      λ0 = eigenvalues[0]
      print("\nA @ v0 =", A @ v0)
      print("λ0 * v0 =", λ0 * v0)
      print("两者相等:", np.allclose(A @ v0, λ0 * v0))
      
      # ─── 2. 验证特征分解 A = P D P^(-1) ────────────────
      P = eigenvectors
      D = np.diag(eigenvalues)
      A_reconstructed = P @ D @ np.linalg.inv(P)
      print("\n重构 A:\n", A_reconstructed)
      print("与原 A 相等:", np.allclose(A, A_reconstructed))
      
      # ─── 3. 用特征分解快速算 A^10 ──────────────────────
      A10_naive = np.linalg.matrix_power(A, 10)
      A10_eigen = P @ np.diag(eigenvalues**10) @ np.linalg.inv(P)
      print("\n两种方法算 A^10 是否一致:", np.allclose(A10_naive, A10_eigen))
      
      # ─── 4. 手写 PCA 并可视化 ───────────────────────────
      np.random.seed(42)
      
      # 生成一些 2D 数据:长椭圆形分布
      n = 200
      mean = [0, 0]
      cov = [[3, 2], [2, 2]]
      data = np.random.multivariate_normal(mean, cov, n)
      
      # Step 1: 中心化
      data_centered = data - data.mean(axis=0)
      
      # Step 2: 协方差矩阵
      C = (data_centered.T @ data_centered) / (n - 1)
      
      # Step 3: 特征分解
      eigvals, eigvecs = np.linalg.eigh(C)   # eigh 用于对称矩阵,更稳定
      
      # Step 4: 按特征值从大到小排序
      idx = np.argsort(eigvals)[::-1]
      eigvals = eigvals[idx]
      eigvecs = eigvecs[:, idx]
      
      print("\n协方差矩阵的特征值:", eigvals)
      print("第一主成分(最大特征值方向):", eigvecs[:, 0])
      print("第二主成分:", eigvecs[:, 1])
      
      # 可视化:原数据 + 两个主成分方向
      plt.figure(figsize=(8, 8))
      plt.scatter(data[:, 0], data[:, 1], alpha=0.5, label='数据')
      
      # 画两个主成分方向(用特征值缩放表示重要程度)
      origin = data.mean(axis=0)
      for i, (val, vec) in enumerate(zip(eigvals, eigvecs.T)):
          plt.arrow(*origin, *(vec * np.sqrt(val) * 2),
                  head_width=0.15, color=['red', 'green'][i],
                  label=f'PC{i+1} (λ={val:.2f})', linewidth=2)
      
      plt.axis('equal')
      plt.grid(True)
      plt.legend()
      plt.title("PCA:特征向量指出数据散布最大的方向")
      plt.show()
      
      # ─── 5. 降维:把 2D 数据投影到 1D ─────────────────
      # 只用第一主成分
      W = eigvecs[:, 0:1]    # shape (2, 1)
      data_1d = data_centered @ W
      print(f"\n降维前 shape: {data.shape}")
      print(f"降维后 shape: {data_1d.shape}")
      
      # 计算保留的"信息比例"
      info_kept = eigvals[0] / eigvals.sum()
      print(f"用 1 维保留了 {info_kept:.1%} 的信息")
      

SVD (奇异值分解)

  • SVD (奇异值分解)
    • 为什么需要 SVD
      • 特征分解 A=PDP−1A = PDP^{-1} 有两个限制
        • 只能用于方阵(行数=列数)
        • 不是所有的方阵都能分解(要求有 n 个线性无关的特征向量)
      • 任何 m×nm\times n 的矩阵都能做 SVD 分解,没有任何限制
    • SVD 公式
      • 对任意矩阵 AA (m×nm\times n) A=UΣVTA=U\Sigma V^T
      • 奇异值通常按从大到小排列:σ1≥σ2≥...≥σr≥0\sigma_1 \ge \sigma_2 \ge ... \ge \sigma _r \ge 0, r 是矩阵的秩
    • 几何解释
      • 矩阵代表线性变换。SVD 说的是——任何线性变换,都可以分解成"旋转 → 缩放 → 旋转"三步
      • 把向量 x⃗\vec{x} 应用矩阵 AA: Ax⃗=UΣVTx⃗A\vec{x} = U\Sigma V^T\vec{x}
      • VTx⃗V^T\vec{x}: 对 x⃗\vec{x} 做 一次旋转/反射
      • Σ(⋅)\Sigma(\cdot): 沿坐标轴各方向独立缩放(缩放倍数=奇异值)
      • U(⋅)U(\cdot): 再做一次旋转/反射
      • 与特征分解的对比
    • 奇异值的含义
      • 衡量矩阵的重要方向
        • 最大的奇异值 σ1\sigma_1 对应矩阵最"主要"的变换方向;后面的奇异值一次描述次要方向。
      • 决定矩阵的秩
        • 非零奇异值的个数 = 矩阵的秩
      • 衡量矩阵的"信息含量”
        • 如果一个矩阵的奇异值快速衰减,说明矩阵的信息几乎全在前两个方向上,后面的方向可以安全丢弃
    • SVD 与 LoRA
      • 问题背景
        • 微调一个 7B 参数的 LLM,要更新所有参数代价极大
        • LoRA 的观察:微调时的参数变化 ΔW\Delta W 通常是低秩的——也就是说,虽然 ΔW\Delta W 是个大矩阵,它真正有效的"主方向"很少
      • LoRA 的做法
        • 不直接学习 ΔW\Delta W,而是学习两个小矩阵 A 和 B ΔW≈BA\Delta W \approx BA
        • 如果 ΔW\Delta W 是 4096x4096,参数量是 1700 万
        • 如果用 LoRA 设秩为 8,则 A 是 8x4096,B 是 4096x8,参数量约 6.5 万,减少 250 倍
    • SVD 在 LLM 中的其他应用
      • 词嵌入分析
        • 对一个 50000×76850000\times 768 的词嵌入矩阵做 SVD,看奇异值衰减情况,可以判断词嵌入空间的有效维度
      • Attention 矩阵分析
        • 研究 Transformer 注意力权重矩阵的奇异值分布,可以诊断模型是否出现"注意力坍缩”——所有注意力都集中在少数几个 token 上
      • 模型压缩
        • 把训练好的权重矩阵做 SVD,丢弃小奇异值,用低秩矩阵替代,可以压缩模型大小
      • 推荐系统的根基
        • 经典的协同过滤推荐算法,本质就是对"用户-物品评分矩阵"做 SVD 低秩近似
    • NumPy 代码实战

      import numpy as np
      import matplotlib.pyplot as plt
      
      # ─── 1. 基本 SVD 分解 ─────────────────────────────────
      np.random.seed(42)
      A = np.random.randn(5, 3)    # 一个 5x3 的非方阵
      
      U, sigma, VT = np.linalg.svd(A, full_matrices=False)
      print("A shape:", A.shape)
      print("U shape:", U.shape)        # (5, 3)
      print("奇异值:", sigma)            # 长度为 3 的数组
      print("V^T shape:", VT.shape)     # (3, 3)
      
      # 验证 A = U Σ V^T
      A_reconstructed = U @ np.diag(sigma) @ VT
      print("\n重构误差:", np.linalg.norm(A - A_reconstructed))
      
      # ─── 2. 验证 U 和 V 是正交矩阵 ────────────────────────
      print("\nU^T @ U =\n", (U.T @ U).round(4))   # 应近似为单位矩阵
      print("\nV @ V^T =\n", (VT @ VT.T).round(4))  # 应近似为单位矩阵
      
      # ─── 3. 低秩近似:用 SVD 压缩图像 ─────────────────────
      # 创建一个简单的"图像"(一个矩阵)
      np.random.seed(0)
      image = np.zeros((50, 50))
      # 加几个明显的结构
      image[10:20, 10:40] = 1
      image[25:35, 15:35] = 0.7
      image[40:48, 5:45] = 0.5
      image += np.random.randn(50, 50) * 0.1
      
      # 对图像做 SVD
      U, sigma, VT = np.linalg.svd(image)
      
      # 用不同 k 值重构
      fig, axes = plt.subplots(1, 5, figsize=(15, 3))
      axes[0].imshow(image, cmap='gray')
      axes[0].set_title(f"原图\n所有 50 个奇异值")
      axes[0].axis('off')
      
      for i, k in enumerate([1, 3, 10, 30]):
          image_k = U[:, :k] @ np.diag(sigma[:k]) @ VT[:k, :]
          axes[i+1].imshow(image_k, cmap='gray')
          info_kept = (sigma[:k]**2).sum() / (sigma**2).sum()
          axes[i+1].set_title(f"k={k}\n信息保留 {info_kept:.1%}")
          axes[i+1].axis('off')
      
      plt.suptitle("SVD 低秩近似:用前 k 个奇异值还原矩阵")
      plt.tight_layout()
      plt.show()
      
      # ─── 4. 奇异值衰减图 ──────────────────────────────────
      plt.figure(figsize=(8, 4))
      plt.plot(sigma, 'o-')
      plt.yscale('log')
      plt.xlabel('奇异值序号')
      plt.ylabel('奇异值大小(对数尺度)')
      plt.title('奇异值衰减情况')
      plt.grid(True)
      plt.show()
      
      # ─── 5. 模拟 LoRA:用低秩分解近似一个权重矩阵 ───────
      # 假设原始权重矩阵
      W = np.random.randn(1024, 1024) * 0.01
      
      # 假设这是"微调后的变化"——故意构造成低秩的
      np.random.seed(1)
      B_true = np.random.randn(1024, 8)
      A_true = np.random.randn(8, 1024)
      delta_W_true = B_true @ A_true   # 真实秩为 8 的变化
      
      # 用 SVD 提取最佳秩-8 近似
      U, sigma, VT = np.linalg.svd(delta_W_true)
      k = 8
      delta_W_approx = U[:, :k] @ np.diag(sigma[:k]) @ VT[:k, :]
      
      print("\n--- LoRA 模拟 ---")
      print(f"原始 ΔW 参数量: {1024 * 1024:,}")
      print(f"LoRA 形式 (B + A) 参数量: {1024 * k + k * 1024:,}")
      print(f"压缩比: {1024 * 1024 / (1024 * k + k * 1024):.1f}x")
      print(f"近似误差: {np.linalg.norm(delta_W_true - delta_W_approx):.6f}")
      
    • SVD vs 特征分解 vs PCA 总结
      • 有趣的关系
        • 对对称正定矩阵,特征分解和 SVD 等价
        • PCA 可以通过对中心化数据矩阵 X 直接做 SVD 实现,比先算协方差矩阵更稳定