系列 · 线性代数 · 第 16 篇

线性代数(十六):深度学习中的线性代数——从全连接到 Transformer

深度学习的核心就是大规模矩阵运算。本章从单个神经元到全连接层的矩阵形式,反向传播的矩阵链式法则,卷积的 im2col 技巧,注意力机制的矩阵操作,到 LoRA 低秩微调。

去掉那些营销包装,深度网络的本质其实很简单:一连串矩阵乘法,中间用逐元素非线性函数连接起来。前向传播、反向传播、卷积、注意力机制、归一化、微调——所有这些所谓的“技巧”不过是同一个代数主题的小小变化。一旦看清背后的矩阵,这个领域就不再是零散的配方,而是统一的语言。

这一章从这种统一的语言出发,重新构建现代深度学习的技术栈。我们会追踪一个信号——向量 $\mathbf{x}$ ——看它如何流经线性层,被卷积处理,被注意力机制关注,被归一化调整,再通过低秩更新进行微调。每一步都会明确指出:哪个矩阵在起作用,以及这个矩阵的什么特性(秩、条件数、转置)让这个操作成功。

你将学到的内容

  • 神经网络本质上就是一系列矩阵乘法——为什么 GPU 上训练离不开批量处理
  • 反向传播就是矩阵链式法则,$W^{\top}$ 是通用的伴随算子
  • 卷积通过 im2col 技巧改写成一次 GEMM 操作
  • 缩放点积注意力分解为四个矩阵步骤,几何意义清晰可见
  • 初始化、归一化和残差连接背后的本质——控制谱半径
  • LoRA:通过低秩更新 $\Delta W = BA$ 实现参数高效的微调

前置知识: 矩阵微积分(第 11 章 )、SVD(第 9 章 )、经典机器学习(第 15 章 )。


线性代数(十六):深度学习中的线性代数——从全连接到 Transformer — 章节概览图

神经网络就是矩阵乘法链#

一个神经元 = 一次内积#

$$h \;=\; \sigma(\mathbf{w}^{\top}\mathbf{x} + b)$$

这就是一次内积加一次非线性。深度学习的其他部分不过是把这个基本操作堆叠和广播而已。

堆叠神经元 = 构造一个矩阵#

$$\mathbf{h} \;=\; \sigma(\mathbf{W}\mathbf{x} + \mathbf{b})$$

从几何上看,$\mathbf{W}$ 是从 $d$ 维输入空间到 $m$ 维特征空间的线性映射;$\sigma$ 则让这个空间变得不再平坦。如果没有 $\sigma$ ,多层线性变换会退化成一个简单的矩阵乘法——正是非线性打破了矩阵乘法的封闭性,才让网络具备了万能逼近能力。

batch 不是可选项,而是必选项#

$$\mathbf{H} \;=\; \sigma(\mathbf{X}\mathbf{W}^{\top} + \mathbf{1}\mathbf{b}^{\top})$$

$B$ 越大,矩阵越大,计算强度越高,硬件效率也越高。

神经网络就是矩阵乘法链

1
2
3
4
5
6
7
8
9
import torch
import torch.nn as nn

linear = nn.Linear(in_features=784, out_features=256)
x_batch = torch.randn(32, 784)   # 32 个样本
h_batch = linear(x_batch)        # (32, 256)

print(linear.weight.shape)       # torch.Size([256, 784])
print(linear.bias.shape)         # torch.Size([256])

训练后的权重矩阵可以解读#

训练完成后,$\mathbf{W}$ 不再是随机噪声——它的每一行是一个模板,对应神经元最敏感的输入模式。把矩阵画成热力图,再把每一行 reshape 回输入的几何形状,就能直接看到网络学到了什么。

解读权重矩阵:每一行就是一个神经元的滤波器

在图像 MLP 上,你会看到方向性边缘和斑点——这些和 Hubel & Wiesel 在 V1 区发现的视觉原语一致。在语言模型中,你会发现对应句法角色的特征方向。矩阵本身是可解释的,只要你愿意去看。

反向传播就是矩阵版的链式法则#

反向传播总被人说得神乎其神。其实没那么复杂,它就是链式法则的矩阵形式,记住一个简单规则就行:前向传播时乘了哪个矩阵,反向传播就乘它的转置。

单层反传四步走#

假设 $\mathbf{z} = \mathbf{W}\mathbf{x} + \mathbf{b}$$\mathbf{h} = \sigma(\mathbf{z})$ ,下游某处有一个标量损失 $L$

  1. 接收上游梯度 $\partial L/\partial \mathbf{h}$
  2. 穿过激活函数(逐元素导数与梯度做 Hadamard 积): $\frac{\partial L}{\partial \mathbf{z}} \;=\; \frac{\partial L}{\partial \mathbf{h}} \odot \sigma'(\mathbf{z})$
  3. 计算参数梯度(外积形式): $\frac{\partial L}{\partial \mathbf{W}} \;=\; \frac{\partial L}{\partial \mathbf{z}}\,\mathbf{x}^{\top}, \qquad \frac{\partial L}{\partial \mathbf{b}} \;=\; \frac{\partial L}{\partial \mathbf{z}}$
  4. 传递给上一层(转置映射): $\frac{\partial L}{\partial \mathbf{x}} \;=\; \mathbf{W}^{\top}\,\frac{\partial L}{\partial \mathbf{z}}$ 为什么是 $\mathbf{W}^{\top}$ ?因为 $\mathbf{W}$ 在前向传播中推动 $\mathbf{x}$ ;它的转置——也就是伴随算子——在反向传播中拉回梯度。这其实就是线性映射的对偶定理,只是用微积分的语言重新包装了一下。

批量形式#

$$\frac{\partial L}{\partial \mathbf{W}} \;=\; \boldsymbol{\Delta}^{\top}\mathbf{X}, \qquad \frac{\partial L}{\partial \mathbf{b}} \;=\; \boldsymbol{\Delta}^{\top}\mathbf{1}, \qquad \frac{\partial L}{\partial \mathbf{X}} \;=\; \boldsymbol{\Delta}\,\mathbf{W}$$

注意,参数梯度其实是所有样本外积的和——一次矩阵乘法就能搞定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import torch
import torch.nn.functional as F

class ManualLinear:
    def __init__(self, in_features, out_features):
        scale = (2 / (in_features + out_features)) ** 0.5
        self.W = torch.randn(out_features, in_features) * scale
        self.b = torch.zeros(out_features)

    def forward(self, x):
        self.x = x
        self.z = x @ self.W.T + self.b
        return F.relu(self.z)

    def backward(self, grad_h):
        grad_z = grad_h * (self.z > 0).float()   # ReLU 导数
        self.grad_W = grad_z.T @ self.x
        self.grad_b = grad_z.sum(dim=0)
        return grad_z @ self.W                    # 传给上一层

雅可比视角#

$$ \nabla_{\!\mathbf{x}} L \;=\; \mathbf{J}^{\top}\,\nabla_{\!\mathbf{y}} L $$

在线性层 $\mathbf{y} = \mathbf{W}\mathbf{x}$ 中,雅可比矩阵就是 $\mathbf{W}$ 本身;在深层网络中,整体雅可比是 $\mathbf{J}_L \mathbf{J}_{L-1} \cdots \mathbf{J}_1$ ,梯度范数被各层算子范数的乘积限制。所有关于“梯度爆炸/消失”的问题,都源于这一行公式。

卷积本质上就是 GEMM#

一维:Toeplitz 矩阵#

$$\mathbf{T} \;=\; \begin{bmatrix} w_2 & w_1 & w_0 & 0 & 0 \\ 0 & w_2 & w_1 & w_0 & 0 \\ 0 & 0 & w_2 & w_1 & w_0 \end{bmatrix}, \qquad \mathbf{y} = \mathbf{T}\mathbf{x}$$

所以卷积本质上就是矩阵乘法。只是没人愿意显式存储 $\mathbf{T}$ ,因为它太大了,而且几乎全是零。

二维:im2col 把卷积变成一次 GEMM#

在二维情况下,框架用同样的方法——im2col。核心思想是把每个感受野展开成一列,拼起来,最终把整个卷积操作变成一次 BLAS 最喜欢的稠密矩阵乘法。

im2col:把卷积变成一次矩阵乘法

具体步骤如下:

  1. 展开输入:对每个输出位置,取出对应的 $C_{\rm in} \times K \times K$ 输入块,拍平后放到 $\mathbf{X}_{\rm col}$ 的一列。
  2. 拉直卷积核:把卷积核展平成一行 $\mathbf{w}_{\rm row}$
  3. 矩阵乘$\mathbf{Y}_{\rm flat} = \mathbf{w}_{\rm row}\,\mathbf{X}_{\rm col}$
  4. 重塑结果:把输出 reshape 回空间形状。 \nim2col 的代价是每个像素会被复制 $K^2$ 次,内存占用增加。但换来的是 cuBLAS 的 GEMM,它经过手工优化,能充分利用 GPU 上的每一个 tensor core。在现代 GPU 上,这种权衡几乎总是值得的。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import torch
import torch.nn.functional as F

def conv2d_via_im2col(x, weight, stride=1, padding=0):
    """用一次 GEMM 实现 2D 卷积"""
    B = x.shape[0]
    C_out, C_in, kh, kw = weight.shape
    if padding > 0:
        x = F.pad(x, [padding] * 4)
    _, _, h_pad, w_pad = x.shape
    out_h = (h_pad - kh) // stride + 1
    out_w = (w_pad - kw) // stride + 1

    col = torch.zeros(B, C_in * kh * kw, out_h * out_w)
    for i in range(out_h):
        for j in range(out_w):
            patch = x[:, :, i*stride:i*stride+kh, j*stride:j*stride+kw]
            col[:, :, i*out_w + j] = patch.reshape(B, -1)

    weight_col = weight.reshape(C_out, -1)
    out = weight_col @ col
    return out.reshape(B, C_out, out_h, out_w)

x = torch.randn(2, 3, 8, 8)
w = torch.randn(16, 3, 3, 3)
print((conv2d_via_im2col(x, w, padding=1) - F.conv2d(x, w, padding=1))
      .abs().max().item())   # ~1e-6

深度可分离卷积 = 低秩分解#

标准 $K\times K$ 卷积的参数量是 $C_{\rm out}\,C_{\rm in}\,K^2$深度可分离卷积把这个张量拆成两部分:

  • Depthwise:每个输入通道单独卷积,不混合通道——参数量 $C_{\rm in}\,K^2$
  • Pointwise($1\times 1$:只混合通道——参数量 $C_{\rm out}\,C_{\rm in}$

这其实是对卷积权重张量的低秩分解。MobileNet、EfficientNet、ConvNeXt 等几乎所有现代轻量视觉模型都依赖这个技巧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class DepthwiseSeparableConv(nn.Module):
    def __init__(self, in_ch, out_ch, k=3, padding=1):
        super().__init__()
        self.depthwise = nn.Conv2d(in_ch, in_ch, k,
                                   padding=padding, groups=in_ch)
        self.pointwise = nn.Conv2d(in_ch, out_ch, 1)

    def forward(self, x):
        return self.pointwise(self.depthwise(x))

print(sum(p.numel() for p in nn.Conv2d(64, 128, 3, padding=1).parameters()))
print(sum(p.numel() for p in DepthwiseSeparableConv(64, 128).parameters()))

注意力 = 用三次矩阵乘法实现软查表#

线性代数(十六):深度学习中的线性代数——从全连接到 Transformer — 章节小结图

图书馆的比喻#

你带着一个问题走进图书馆。每本书都有一个(关键词)和一个(内容)。你把自己的查询和每本书的键逐一比较,计算出分数后归一化为权重,最后根据权重对所有值加权融合。

这就是注意力机制的核心思想。它的巧妙之处在于,整个过程都可以用矩阵乘法干净利落地表达出来。

缩放点积注意力,四步拆解#

$$\mathrm{Attention}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) \;=\; \mathrm{softmax}\!\left(\frac{\mathbf{Q}\mathbf{K}^{\top}}{\sqrt{d_k}}\right)\mathbf{V}$$

从左到右看这四个步骤:

缩放点积注意力的四步矩阵分解

  1. $\mathbf{Q}\mathbf{K}^{\top}$ ——计算所有 query-key 对的点积,得到一个 $n\times n$ 的相似度矩阵。
  2. 除以 $\sqrt{d_k}$ 。如果 $\mathbf{Q},\mathbf{K}$ 的元素独立同分布于 $\mathcal{N}(0,1)$ ,点积的方差是 $d_k$ 。如果不缩放,大的 $d_k$ 会让 softmax 饱和,梯度消失;除以 $\sqrt{d_k}$ 把方差拉回 1,让 softmax 处于良态。
  3. 按行做 softmax,把分数转换成概率分布——这就是注意力权重
  4. 乘以 $\mathbf{V}$ 。每行输出是所有 value 向量的凸组合,权重就是相关性。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import torch
import torch.nn.functional as F
import math

def scaled_dot_product_attention(Q, K, V, mask=None):
    """Q, K, V: (B, h, n, d_k)"""
    d_k = Q.size(-1)
    scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, float('-inf'))
    weights = F.softmax(scores, dim=-1)
    return weights @ V, weights

多头:并行处理多个子空间#

$$\mathrm{MultiHead}(\mathbf{X}) = \mathrm{Concat}(\text{head}_1, \ldots, \text{head}_h)\,\mathbf{W}^O$$ $$\text{head}_i = \mathrm{Attention}(\mathbf{X}\mathbf{W}_i^Q,\,\mathbf{X}\mathbf{W}_i^K,\,\mathbf{X}\mathbf{W}_i^V)$$

投影矩阵 $\mathbf{W}^Q, \mathbf{W}^K, \mathbf{W}^V$$d_{\rm model}$ 维的 embedding 切分成 $h$ 个互不重叠的子空间;$\mathbf{W}^O$ 再把多头输出拼接回去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        assert d_model % n_heads == 0
        self.d_k = d_model // n_heads
        self.n_heads = n_heads
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def forward(self, Q, K, V, mask=None):
        B = Q.size(0)
        Q = self.W_q(Q).view(B, -1, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_k(K).view(B, -1, self.n_heads, self.d_k).transpose(1, 2)
        V = self.W_v(V).view(B, -1, self.n_heads, self.d_k).transpose(1, 2)
        out, w = scaled_dot_product_attention(Q, K, V, mask)
        out = out.transpose(1, 2).contiguous().view(B, -1, self.n_heads * self.d_k)
        return self.W_o(out), w

朴素注意力的时间复杂度是 $O(n^2 d_k)$ ,内存占用是 $O(n^2)$ ——长序列下 $n\times n$ 的得分矩阵成为瓶颈。FlashAttention 通过分块重新组织计算,避免这个矩阵进入 HBM;数学上完全等价,但对内存层级更友好。

组合起来:一个 Transformer Block#

Transformer 的编码器层其实就四个固定部分。

  • 多头自注意力第 4 节 )。
  • 逐位置 FFN:一个两层的 MLP,对每个 token 独立操作,先扩展再投影: $\mathrm{FFN}(\mathbf{x}) = \mathbf{W}_2\,\mathrm{ReLU}(\mathbf{W}_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2$
  • 残差连接:每个子层输出 $\mathbf{x} + \mathrm{sublayer}(\mathbf{x})$ 。雅可比矩阵变成 $\mathbf{I} + \mathbf{J}$ ,特征值集中在 1 附近——梯度始终有条畅通的反向捷径。
  • 层归一化第 6 节 )。

解码器多了交叉注意力(query 来自解码器,key 和 value 来自编码器)和一个因果 mask,把未来位置的得分置零:

1
2
def causal_mask(seq_len):
    return torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0).unsqueeze(0)
$$\mathrm{PE}_{(\mathrm{pos}, 2i)} = \sin(\mathrm{pos}/10000^{2i/d}), \qquad \mathrm{PE}_{(\mathrm{pos}, 2i+1)} = \cos(\mathrm{pos}/10000^{2i/d})$$

它有个优雅的性质:$\mathrm{PE}_{\mathrm{pos}+k}$$\mathrm{PE}_{\mathrm{pos}}$ 的线性函数——相对位置在特征空间里表现为固定的旋转。


归一化:沿不同轴做标准化#

训练时,激活值会漂移。归一化层在每次前向传播时,把它们拉回到一个已知的分布。

BatchNorm 和 LayerNorm 的区别#

两者都先计算 $\hat{x} = (x - \mu)/\sqrt{\sigma^2 + \epsilon}$ ,然后通过可学习的仿射变换 $\gamma\hat{x} + \beta$ 调整。它们的区别在于均值和方差是沿哪个维度计算的

  • BatchNorm:对每个特征,在 batch 维度上求均值和方差。它把样本绑在一起,估计的质量依赖于 batch 大小;推理时需要使用训练过程中累积的滑动平均。
  • LayerNorm:对每个样本,在特征维度上求均值和方差。它不依赖样本,也不依赖 batch 大小,还能轻松并行——这就是为什么所有 Transformer 都用它。

从矩阵的角度看:BatchNorm 标准化的是激活矩阵的,而 LayerNorm 标准化的是

RMSNorm:去掉均值计算#

$$\hat{x} = \frac{x}{\mathrm{RMS}(x)} \cdot \gamma, \qquad \mathrm{RMS}(x) = \sqrt{\tfrac{1}{d}\sum_i x_i^2}$$

计算量比 LayerNorm 少了一半左右,效果却差不多。现代 LLM 几乎全都采用了这种做法。

初始化、条件数与梯度流动#

所有训练深度模型时遇到的问题,最终都能归结到一个关键量:各层雅可比矩阵的奇异值连乘积

方差守恒原则#

想让信号通过 $L$ 层网络后既不爆炸也不消失?那就要求每一层尽量保持方差不变。对于线性层 $\mathbf{y} = \mathbf{W}\mathbf{x}$ ,如果输入和权重都是零均值独立同分布(i.i.d.),那么 $\mathrm{Var}(y_i) = n_{\rm in}\,\mathrm{Var}(W_{ij})\,\mathrm{Var}(x)$ 。将这个值设为等于 $\mathrm{Var}(x)$ ,就能推导出初始化规则。

  • Xavier(Glorot)初始化,适用于 $\tanh$ /sigmoid 激活函数: $w_{ij} \sim \mathcal{U}\!\left[-\sqrt{\tfrac{6}{n_{\rm in} + n_{\rm out}}},\;\sqrt{\tfrac{6}{n_{\rm in} + n_{\rm out}}}\right]$
  • He(Kaiming)初始化,专为 ReLU 设计(ReLU 会杀死一半神经元,导致方差减半,因此需要将尺度放大两倍): $w_{ij} \sim \mathcal{N}\!\left(0,\,\tfrac{2}{n_{\rm in}}\right)$

谱分布揭示的真相#

矩阵乘积 $\mathbf{W}_L \mathbf{W}_{L-1} \cdots \mathbf{W}_1$ 的最大奇异值大约是 $\prod_\ell \sigma_{\max}(\mathbf{W}_\ell)$ 。如果每个因子的 $\sigma_{\max} > 1$ ,乘积会迅速爆炸;如果每个因子的 $\sigma_{\max} < 1$ ,乘积则会坍缩至零。He 和 Xavier 初始化的核心思想就是让每个因子的最大奇异值接近 1。

为什么初始化重要:奇异值分布对比

右图清楚地展示了问题:朴素的 $\mathcal{N}(0,1)$ 初始化每增加一层,数值就会爆炸几个数量级;而 He 初始化无论网络多深,乘积的最大奇异值始终稳定在 $O(1)$

梯度视角下的相同规律#

用三种不同的初始化方法跑一遍 6 层线性网络的前向和反向传播:

深网络中的梯度流动

朴素初始化导致梯度爆炸,过小初始化让梯度消失,只有 He 初始化能让激活值和梯度平稳地穿过每一层。现代深度学习的各种技巧——残差连接、精细初始化、归一化层、梯度裁剪——本质上都在做一件事:让每层雅可比矩阵的谱半径接近 1


LoRA:微调就是低秩更新#

前沿 LLM 动辄几百亿参数。全量微调在算力、内存和存储上都太浪费了——每个任务都要存一份完整的权重。LoRA 的核心观察是:真正需要的更新往往是低秩的,即使基座权重不是。

公式#

$$\mathbf{W}' \;=\; \mathbf{W}_0 + \Delta\mathbf{W}, \qquad \Delta\mathbf{W} = \mathbf{B}\mathbf{A}$$

其中 $\mathbf{A} \in \mathbb{R}^{r \times d_{\rm in}}$$\mathbf{B} \in \mathbb{R}^{d_{\rm out} \times r}$ ,且 $r \ll \min(d_{\rm in}, d_{\rm out})$

参数量
单层全量微调$d_{\rm in}\,d_{\rm out}$
LoRA(秩 $r$$r\,(d_{\rm in} + d_{\rm out})$

假设 $d_{\rm in} = d_{\rm out} = 4096$$r = 8$ :原本 1677 万参数减少到 6.5 万——压缩了 256 倍

LoRA:权重更新的低秩分解

上图展示了分解过程:左边一块矮胖的 $\Delta\mathbf{W}$ ,等于右边一个高瘦的 $\mathbf{B}$ 乘一个矮宽的 $\mathbf{A}$ 。左下图显示参数节省,右下图展示重构误差——一旦秩 $r$ 达到更新的真实内禀秩,误差迅速降为 0。

为什么低秩有效#

经验研究表明(Aghajanyan 等、Hu 等),微调引起的权重变化通常位于一个低维子空间中——这就是所谓的“内禀维度”假设。LoRA 直接限制 $\mathrm{rank}(\Delta\mathbf{W}) \le r$ ,既节省参数,又起到结构正则化的作用:调整方向被限制在 $r$ 个方向内,避免了全量微调常见的灾难性遗忘问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class LoRALinear(nn.Module):
    def __init__(self, base_layer: nn.Linear, r=8, alpha=16):
        super().__init__()
        self.base = base_layer
        for p in self.base.parameters():
            p.requires_grad = False
        d_in, d_out = base_layer.in_features, base_layer.out_features
        self.A = nn.Parameter(torch.randn(r, d_in) * 0.01)
        self.B = nn.Parameter(torch.zeros(d_out, r))   # B 零初始化 -> 初始等价于原模型
        self.scaling = alpha / r

    def forward(self, x):
        return self.base(x) + (x @ self.A.T @ self.B.T) * self.scaling

    def merge(self):
        """推理时把 BA 合并到基座权重,零额外开销。"""
        self.base.weight.data += (self.B @ self.A) * self.scaling

LoRA 家族#

  • QLoRA:用 4-bit NF4 存储 $\mathbf{W}_0$ ,LoRA 用 BF16 训练。单张 48GB 显卡就能微调 65B 模型。
  • DoRA:将权重列分解为模长 $m$ 和方向 $\hat{v}$ ;方向用 LoRA 适配,模长单独学习。
  • AdaLoRA:通过敏感性分析估计每层重要性,动态分配秩预算。

这些方法本质上都是线性代数操作:LoRA 是秩约束,DoRA 是极分解,AdaLoRA 是秩分配。同一套代数工具,不同调节方式。

全局视角#

把整章内容整理成一张表:

操作线性代数原语为什么有效
全连接层矩阵-向量乘积线性变换 + 非线性激活
反向传播转置矩阵($W^{\top}$链式法则 = 雅可比矩阵连乘
卷积(im2col)块 Toeplitz $\to$ GEMM用内存换取 cuBLAS 的高吞吐
深度可分离卷积低秩张量分解减少 FLOPs 和参数量
注意力机制$\mathrm{softmax}(QK^{\top}/\sqrt{d_k})V$内容寻址的软查询
多头机制$h$ 个子空间的直和并行提取多种相关模式
残差连接$\mathbf{I} + \mathbf{J}$特征值集中在 1 附近
BN / LN / RMSNorm行 vs 列标准化控制激活值的尺度
Xavier / He 初始化方差保持每层最大奇异值 $\sigma_{\max}$ 接近 1
LoRA$\Delta W = BA$$\mathrm{rank} \le r$利用低维内禀结构

现代深度学习的所有操作,本质上就是在线性映射、转置、秩、奇异值这些核心概念之间来回组合。架构花样翻新,但底层原语始终不变。

练习题#

基础题#

  1. 如果 $\mathbf{W}$ 是正交矩阵($\mathbf{W}^{\top}\mathbf{W} = \mathbf{I}$ ),证明线性层 $\mathbf{h} = \mathbf{W}\mathbf{x}$ 在前向和反向传播中都能保持 $\ell_2$ 范数。
  2. 输入是 $\mathbb{R}^{100}$ ,经过一个 MLP $100 \to 256 \to 128 \to 10$ 。写出每个权重矩阵的形状,并计算总参数量(别忘了偏置项)。
  3. 多头注意力为什么选择 $d_k = d_{\rm model}/h$ 而不是每个头都用完整的 $d_{\rm model}$ ?比较两种方式的参数量和表达能力。
  4. 对于形状为 $(B, C, H, W)$ 的卷积特征图,BatchNorm 和 LayerNorm 分别在哪些维度上计算均值和方差?

进阶题#

  1. 假设 $\mathbf{Q}$$\mathbf{K}$ 的每个元素独立同分布于 $\mathcal{N}(0,1)$ 。计算 $\mathbf{Q}\mathbf{K}^{\top}$ 中单个元素的均值和方差,并用结果解释为什么需要 $\sqrt{d_k}$ 缩放。
  2. im2col 实现:输入形状为 $(1, 3, 8, 8)$ ,卷积核形状为 $(16, 3, 3, 3)$ ,步长为 1,填充为 1。计算 $\mathbf{X}_{\rm col}$ 的形状以及内存膨胀倍数。
  3. 证明:如果 $\Delta\mathbf{W} = \mathbf{B}\mathbf{A}$ ,其中 $\mathbf{B}\in\mathbb{R}^{m\times r}$$\mathbf{A}\in\mathbb{R}^{r\times n}$ ,那么 $\mathrm{rank}(\Delta\mathbf{W}) \le r$
  4. 分析 ResNet 残差块 $\mathbf{y} = \mathbf{x} + F(\mathbf{x})$ 的梯度流。证明反向传播时存在一条“捷径”路径,让梯度无需经过 $F$ 就能传递到前面。

编程题#

  1. 实现一个多层 Transformer encoder,并在一个简单的序列分类任务上测试。
  2. 对一个小规模预训练模型应用 LoRA。报告参数量、训练显存占用和下游任务精度,并与全量微调对比。
  3. 随机生成一个 batch,可视化 BatchNorm、LayerNorm 和 RMSNorm 前后的激活值分布变化。
  4. 使用 BERT-tiny,按组件拆解 FLOPs(attention、FFN、归一化)。绘制总算力随序列长度变化的曲线。

开放题#

  1. 为什么 Transformer 普遍使用 LayerNorm 而不是 BatchNorm?从训练稳定性、变长序列支持和并行计算的角度分析。
  2. LoRA 假设微调更新是低秩的。描述一个可能让这个假设失效的场景,并设计实验来检测它。
  3. 把 2D CNN 迁移到 3D 数据(如视频或医学影像)上,FLOPs 和参数量如何变化?从矩阵代数的角度给出解释。

总结#

  • 神经网络的每一层,本质上就是矩阵乘法加非线性变换。Batch 的引入把它们整合成一次大规模 GEMM 操作,而这正是 GPU 最擅长的任务。
  • 反向传播的核心是矩阵链式法则。前向映射的转置直接对应反向映射——这就是伴随对偶的本质,没有更多可说的。
  • 卷积通过 im2col 方法可以转化为 GEMM。深度可分离卷积则是对卷积张量进行低秩分解的结果。
  • 注意力机制是一种软查表操作:$\mathrm{softmax}(QK^{\top}/\sqrt{d_k})V$ 。多头注意力则将这一过程并行化到多个子空间中。
  • 初始化、归一化和残差连接的设计目标其实只有一个:让每层雅可比矩阵的谱半径接近 1。
  • LoRA 借助了微调更新中内禀秩较低的经验特性:$\Delta W = BA$ ,其中 $r \ll \min(d_{\rm in}, d_{\rm out})$

只要掌握了这些基本原理,下次看到新架构时,我不会觉得它陌生——它不过是熟悉元素的重新组合罢了。


参考文献#

  • Vaswani, A. et al. “Attention Is All You Need.” NeurIPS 2017.
  • Hu, E. et al. “LoRA: Low-Rank Adaptation of Large Language Models.” ICLR 2022.
  • Aghajanyan, A. et al. “Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning.” ACL 2021.
  • Ba, J., Kiros, J., Hinton, G. “Layer Normalization.” arXiv 2016.
  • Zhang, B., Sennrich, R. “Root Mean Square Layer Normalization.” NeurIPS 2019.
  • Ioffe, S., Szegedy, C. “Batch Normalization.” ICML 2015.
  • He, K. et al. “Deep Residual Learning for Image Recognition.” CVPR 2016.
  • He, K. et al. “Delving Deep into Rectifiers.” ICCV 2015.(He 初始化)
  • Glorot, X., Bengio, Y. “Understanding the Difficulty of Training Deep Feedforward Neural Networks.” AISTATS 2010.(Xavier 初始化)
  • Dao, T. et al. “FlashAttention.” NeurIPS 2022.
  • Howard, A. et al. “MobileNets.” arXiv 2017.(深度可分离卷积)
本系列

线性代数 18 篇

  1. 01 线性代数(一):向量的本质——不仅仅是箭头
  2. 02 线性代数(二):线性组合与向量空间
  3. 03 线性代数(三):矩阵作为线性变换
  4. 04 线性代数(四):行列式的秘密
  5. 05 线性代数(五):线性方程组与列空间
  6. 06 线性代数(六):特征值与特征向量
  7. 07 线性代数(七):正交性与投影——当向量互不干扰
  8. 08 线性代数(八):对称矩阵与二次型
  9. 09 线性代数(九):奇异值分解 SVD
  10. 10 线性代数(十):矩阵范数与条件数——数值计算的健康体检
  11. 11 线性代数(十一):矩阵微积分与优化——从梯度到反向传播
  12. 12 线性代数(十二):稀疏矩阵与压缩感知——少即是多的数学奇迹
  13. 13 线性代数(十三):张量与多线性代数——从标量到高维数据立方体
  14. 14 线性代数(十四):随机矩阵理论——混沌中的秩序
  15. 15 线性代数(十五):机器学习中的线性代数——从 PCA 到推荐系统
  16. 16 线性代数(十六):深度学习中的线性代数——从全连接到 Transformer 当前
  17. 17 线性代数(十七):计算机视觉中的线性代数——从像素到三维重建
  18. 18 线性代数(十八):前沿应用与总结——量子计算、GNN、大模型,与十八章回望

读有所得?

GitHub 关注我 → 新文周更

GitHub