Series · Linear Algebra · Chapter 16

深度学习中的线性代数 -- 从全连接到 Transformer

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

把所有营销话术都剥掉,深度学习就剩一件事:一长串矩阵乘法,中间夹一些逐元素的非线性。前向传播、反向传播、卷积、注意力、归一化、参数高效微调——每一个"花招"都不过是同一套代数主题的小变奏。一旦你能直接看见这些矩阵,整个领域就会从一袋零散食谱变成一种统一语言。

本章用这种统一的视角把现代深度学习重建一遍。我们跟随同一个信号——一个向量 $\mathbf{x}$——看它如何穿过线性层,被卷积,被注意,被归一化,被低秩更新所微调。每一步都明确指出:是哪个矩阵在干活,又是这个矩阵的哪个性质(秩、条件数、转置)让这个技巧成立。

本章你将学到

  • 神经网络如何"就是"一串矩阵乘法——以及为什么 GPU 训练几乎离不开 batch
  • 反向传播是矩阵链式法则,$W^{\top}$ 是普适的"伴随算子"
  • im2col:把卷积改写成一次 GEMM,让 cuBLAS 全速跑起来
  • 缩放点积注意力四步矩阵分解,能直接读出每一步的几何含义
  • 初始化、归一化、残差连接其实都是同一件事——控制每层雅可比的谱半径
  • LoRA:把微调写成低秩更新 $\Delta W = BA$

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


1. 神经网络就是矩阵乘法链

1.1 一个神经元 = 一次内积

神经元做的事情简单到不能再简单:对输入做加权求和,加偏置,过一个非线性。

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

一次内积加一次非线性,仅此而已。把这个原语反复堆叠和广播,就是整个深度学习的全部把戏。

1.2 把神经元堆起来 = 一个矩阵

把 $m$ 个神经元的权重向量按行叠成矩阵 $\mathbf{W} \in \mathbb{R}^{m \times d}$:

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

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

1.3 batch 不是可选项,是必选项

GPU 是被 GEMM 喂大的。单个样本几乎浪费了所有 FLOPs。把 $B$ 个样本按行堆成 $\mathbf{X} \in \mathbb{R}^{B \times d}$,整层就变成一次大矩阵乘法:

$$\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])

1.4 训练后的权重矩阵是可读的

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

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

图像 MLP 上你会看到方向性边缘和斑点——和 Hubel & Wiesel 在 V1 找到的视觉原语一样。语言模型里你会发现对应句法角色的特征方向。矩阵是可解释的,前提是你愿意去看。


2. 反向传播 = 矩阵版的链式法则

反向传播在科普文章里常被神秘化。其实它就是链式法则的矩阵写法,记住一个口诀:前向乘了哪个矩阵,反向就乘它的转置。

2.1 单层反传四步走

设 $\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})$$
  1. 算参数梯度(外积形式):
$$\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}}$$
  1. 传给上一层(转置映射):
$$\frac{\partial L}{\partial \mathbf{x}} \;=\; \mathbf{W}^{\top}\,\frac{\partial L}{\partial \mathbf{z}}$$

为什么是 $\mathbf{W}^{\top}$?因为 $\mathbf{W}$ 把 $\mathbf{x}$ 推到前面去;它的转置——也就是伴随算子——把梯度拉回来。这正是线性映射的对偶定理,只是换了一身微积分的衣服。

2.2 批量形式

batch $\mathbf{X} \in \mathbb{R}^{B \times d}$,激活后的梯度记作 $\boldsymbol{\Delta}$:

$$\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                    # 传给上一层

2.3 雅可比视角

对任意 $\mathbf{y} = f(\mathbf{x})$,雅可比矩阵 $\mathbf{J}_{ij} = \partial y_i / \partial x_j$ 就是局部线性近似。链式法则因此成了雅可比的乘积:

$$\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$,梯度范数被各层算子范数的乘积所限制。后面所有"梯度爆炸/消失"的故事,都从这一行公式里长出来。


3. 卷积只是披着外套的 GEMM

3.1 一维:Toeplitz 矩阵

一维卷积 $\mathbf{w} = [w_0, w_1, w_2]$ 作用在长度 5 的输入上,等价于乘一个带状的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}$ 太大、几乎全是 0,没人愿意显式存它。

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

二维情况下,框架用同样的思路——im2col:把每个感受野展开成一列,所有列拼成一个稠密矩阵,整个卷积就退化为一次 BLAS 最爱的稠密矩阵乘法。

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

具体步骤:

  1. 展开输入:每个输出位置对应一个 $C_{\rm in} \times K \times K$ 的 patch,把它拍平成 $\mathbf{X}_{\rm col}$ 的一列。
  2. 展开卷积核:把 kernel 拉直成一行 $\mathbf{w}_{\rm row}$。
  3. 矩阵乘:$\mathbf{Y}_{\rm flat} = \mathbf{w}_{\rm row}\,\mathbf{X}_{\rm col}$。
  4. reshape 回空间形状。

代价是 im2col 把每个像素复制了 $K^2$ 次,内存膨胀。但换来的是 cuBLAS GEMM——这是被人手工调教到喂饱每一个 SM 上 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

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

标准 $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
13
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()))
# 73,856   vs   8,896  -> 参数减少约 8 倍

4. 注意力 = 用三次矩阵乘法做软查表

4.1 图书馆的比喻

你带着一个问题走进图书馆。每本书都有(关键词)和(内容)。你拿你的查询和每个键比对,把分数归一化为权重,再用权重对所有值做加权融合。

注意力机制就是这个动作。妙的是这一切都能用矩阵乘法干净地写出来。

4.2 缩放点积注意力,四步看穿

$$\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}$ 元素 i.i.d. $\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

4.3 多头:在多个子空间并行

单头注意力只能学到一种"相关"的概念。真实语言需要很多种——句法的、语义的、位置的。多头注意力把 $h$ 个头并行跑,每个头在 $d_k = d_{\rm model}/h$ 的子空间里独立工作,最后拼起来再混合。

$$\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;数学上完全等价,只是对内存层级更友好。


5. 把一切拼起来:一个 Transformer Block

一个 Transformer encoder 层就是四个固定的成分。

  • 多头自注意力(第 4 节)。
  • 逐位置 FFN:一个两层 MLP,对每个 token 独立做"扩张-压缩":
$$\mathrm{FFN}(\mathbf{x}) = \mathbf{W}_2\,\mathrm{ReLU}(\mathbf{W}_1\mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2$$
  • 残差连接:每个 sublayer 输出 $\mathbf{x} + \mathrm{sublayer}(\mathbf{x})$。雅可比变成 $\mathbf{I} + \mathbf{J}$,特征值聚集在 1 附近——梯度永远有一条直通后方的捷径。
  • 层归一化(第 6 节)。

Decoder 多了交叉注意力(query 来自 decoder,key/value 来自 encoder)和一个因果 mask,把"未来位置"的得分置为 $-\infty$:

1
2
def causal_mask(seq_len):
    return torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0).unsqueeze(0)

由于没有循环结构,模型需要位置编码才能知道每个 token 的位置。正弦位置编码

$$\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}}$ 的线性函数——相对位置在特征空间里就是一个固定的旋转。


6. 归一化:沿不同的轴做标准化

训练过程中激活会漂移,归一化层在每次前向时把它们拉回已知分布。

6.1 BatchNorm vs LayerNorm

两者都做 $\hat{x} = (x - \mu)/\sqrt{\sigma^2 + \epsilon}$,再来一次可学习仿射 $\gamma\hat{x} + \beta$。关键区别在于均值方差沿哪个轴算

  • BatchNorm:对每个特征,在 batch 维度上求均值方差。把不同样本绑在一起;估计精度依赖 batch 大小;推理时需要使用训练期累计的滑动平均。
  • LayerNorm:对每个样本,在特征维度上求均值方差。与 batch 无关,与 batch 大小无关,天然可并行——这正是 Transformer 普遍选它的原因。

记住矩阵心智模型:BatchNorm 标准化激活矩阵的,LayerNorm 标准化激活矩阵的

6.2 RMSNorm:把均值也省掉

LLaMA 系列把 LayerNorm 进一步简化——不减均值,只用均方根缩放:

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

FLOPs 大致减半,下游效果相当。现代 LLM 几乎一边倒地采用它。


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

所有"训不深"的问题,归根结底都归到一个量上:各层雅可比的奇异值连乘

7.1 方差守恒原则

想让信号穿过 $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 杀掉一半神经元,方差被砍半,所以幅度乘 2 补回来):
$$w_{ij} \sim \mathcal{N}\!\left(0,\,\tfrac{2}{n_{\rm in}}\right)$$

7.2 谱告诉你的故事

乘积 $\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)$。

7.3 同一个故事,从梯度看

跑一下 6 层线性网络的前向和反向:

深网络中的梯度流动

朴素初始化爆炸,过小初始化消失,只有 He 初始化能让激活和梯度都平稳穿过所有层。所有现代深度训练技巧——残差连接、精巧初始化、归一化层、梯度裁剪——本质上是同一个目标:让每层雅可比的谱半径靠近 1


8. LoRA:把微调写成低秩更新

前沿 LLM 动辄几百亿参数,全量微调在算力、内存、存储(每个任务一份完整权重)三方面都是浪费。LoRA 的洞察是:你真正需要的更新往往是低秩的,即便基座权重不是。

8.1 公式

冻结预训练权重 $\mathbf{W}_0$,只学一个加性更新,并把它分解成两个"瘦长"矩阵:

$$\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。

8.2 为什么低秩有效

经验研究(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 折进 base 权重,零额外开销。"""
        self.base.weight.data += (self.B @ self.A) * self.scaling

8.3 LoRA 家族

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

它们本质都是线性代数手术:LoRA 是秩约束,DoRA 是极分解,AdaLoRA 是秩分配。同一套代数,不同旋钮。


9. 全章鸟瞰

把整章拉成一张表:

操作线性代数原语为什么有效
全连接层矩阵-向量乘线性映射 + 非线性
反向传播伴随算子($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$。写出每个权重矩阵的形状,并算出总参数量(别忘了 bias)。
  3. 多头注意力为什么用 $d_k = d_{\rm model}/h$ 而不是每个头都用 $d_{\rm model}$?比较参数量和表达能力。
  4. 对形状 $(B, C, H, W)$ 的卷积特征图,BN 和 LN 分别在哪些维度上算均值方差?

进阶

  1. 假设 $\mathbf{Q}, \mathbf{K}$ 元素 i.i.d. $\mathcal{N}(0,1)$。计算 $\mathbf{Q}\mathbf{K}^{\top}$ 单个元素的均值和方差,并用结果论证 $\sqrt{d_k}$ 缩放的必要性。
  2. im2col:输入 $(1, 3, 8, 8)$、kernel $(16, 3, 3, 3)$、stride 1、padding 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,可视化 BN/LN/RMSNorm 前后的激活分布变化。
  4. 拿 BERT-tiny 做 FLOPs 拆解(attention vs FFN vs 归一化),画总算量随序列长度变化的曲线。

开放性

  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.(深度可分离卷积)

系列导航

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub