线性代数(十六)深度学习中的线性代数
Chen Kai BOSS

深度学习的核心,说白了就是大规模的矩阵运算。无论是最简单的全连接网络,还是复杂的 Transformer,背后都是线性代数在支撑。理解这一点,不仅能让你更好地调试模型、优化性能,还能帮你设计出更高效的网络架构。

神经网络的矩阵表示

从单个神经元说起

一个神经元做的事情非常简单:接收若干输入,加权求和,加上偏置,再通过激活函数。用数学表达就是:

$$

y = (w_1 x_1 + w_2 x_2 + + w_n x_n + b) $$

这个求和过程,其实就是向量的内积。如果我们把权重写成行向量 ,输入写成列向量 ,那么:

$$

y = ( + b) $$

一个神经元 = 一次内积 + 一次非线性变换。就这么简单。

一层神经元的矩阵形式

现在假设一层有 个神经元,每个神经元有自己的权重向量。把这些权重向量按行堆叠起来,就构成了权重矩阵

$$

W =

^{m n} $$

这样,一层的前向传播就可以写成:

其中 是这一层的输出, 是偏置向量, 对向量的每个元素独立作用。

直觉理解:权重矩阵 将输入从 维空间映射到 维空间。这是一个线性变换,激活函数 引入非线性,使得网络能够学习复杂的函数。

批量处理( Batch Processing)

实际训练时,我们不会一次只处理一个样本。假设有 个样本,每个样本是 维向量,我们把它们按行排列成矩阵

$$

X =

$$

一层的批量前向传播变成:

$$

H = (XW^T + ^T) $$

这里 是全 1 向量, 是将偏置广播到每个样本。

为什么要批量处理? 因为 GPU 擅长大规模并行计算。单个样本的计算无法充分利用 GPU 的并行能力,批量处理可以让矩阵乘法的规模变大,从而充分利用硬件。

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

# 定义一个简单的全连接层
linear = nn.Linear(in_features=784, out_features=256)

# 单个样本
x_single = torch.randn(784)
h_single = linear(x_single) # 输出形状: (256,)

# 批量样本
x_batch = torch.randn(32, 784) # 32 个样本
h_batch = linear(x_batch) # 输出形状: (32, 256)

print(f"权重矩阵形状: {linear.weight.shape}") # (256, 784)
print(f"偏置向量形状: {linear.bias.shape}") # (256,)

多层网络的矩阵链

多层神经网络就是多个这样的变换串联起来:

如果没有激活函数,这就是矩阵连乘 ,等价于一个矩阵 。所以没有非线性,多层网络和单层没区别。

非线性的作用:打破矩阵乘法的封闭性,使得网络能够逼近任意连续函数(万能逼近定理)。

反向传播的矩阵形式

反向传播是深度学习的核心算法。从线性代数的角度看,它就是链式法则的矩阵版本。

单层的梯度

考虑一层 ,设损失函数为 。我们需要计算 (用于传给前一层)。

(激活前的值),

第一步:假设我们已经知道 (从后一层传回来的梯度)。

第二步:通过激活函数反传。如果 是逐元素函数:

其中 表示逐元素乘积, 是激活函数的导数。

第三步:计算参数梯度。这是关键的矩阵运算:

第四步:传给前一层:

直觉理解 的出现是因为前向传播时 变换到 ,反向传播时需要沿着"转置的路径"传回去。

批量反向传播

对于批量数据 ,前向传播为

是损失对输出的梯度, 是通过激活函数后的梯度。

权重梯度需要对所有样本求和:

偏置梯度是对批量维度求和:

传给前一层的梯度:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import torch
import torch.nn.functional as F

# 手动实现一层的前向和反向传播
class ManualLinear:
def __init__(self, in_features, out_features):
# Xavier 初始化
self.W = torch.randn(out_features, in_features) * (2 / (in_features + out_features)) ** 0.5
self.b = torch.zeros(out_features)
self.W.requires_grad = True
self.b.requires_grad = True

def forward(self, x):
self.x = x # 保存用于反向传播
self.z = x @ self.W.T + self.b
self.h = F.relu(self.z)
return self.h

def backward(self, grad_h):
# 通过 ReLU 反传
grad_z = grad_h * (self.z > 0).float()

# 计算参数梯度
grad_W = grad_z.T @ self.x # (out, batch) @ (batch, in) = (out, in)
grad_b = grad_z.sum(dim=0)

# 传给前一层
grad_x = grad_z @ self.W # (batch, out) @ (out, in) = (batch, in)

# 保存梯度
self.W.grad = grad_W
self.b.grad = grad_b

return grad_x

# 验证
layer = ManualLinear(784, 256)
x = torch.randn(32, 784)
h = layer.forward(x)
grad_h = torch.randn_like(h)
grad_x = layer.backward(grad_h)

print(f"输入梯度形状: {grad_x.shape}") # (32, 784)
print(f"权重梯度形状: {layer.W.grad.shape}") # (256, 784)

雅可比矩阵视角

更一般地,如果 ,其中 Double exponent: use braces to clarify f: ^n ^m,雅可比矩阵定义为:

$$

J = {} =

^{m n} $$

链式法则的矩阵形式就是雅可比矩阵的乘积:

对于线性层 ,雅可比矩阵就是 本身。这解释了为什么反向传播要用

卷积的矩阵形式

卷积神经网络( CNN)是图像处理的核心。虽然卷积看起来和矩阵乘法不一样,但它可以转化为矩阵乘法。

一维卷积

一维离散卷积的定义:

在深度学习中,我们处理的是有限长度的信号和卷积核:

$$

y[n] = _{k=0}^{K-1} w[k] x[n+k] $$

其中 是长度为 的卷积核, 是输入信号。

矩阵形式:一维卷积可以表示为托普利茨( Toeplitz)矩阵乘法。假设输入 ,卷积核 ,输出长度为

构造矩阵

$$

T =

$$

二维卷积

二维卷积用于图像处理:

$$

Y[i,j] = {m=0}^{K_h-1} {n=0}^{K_w-1} W[m,n] X[i+m, j+n] $$

im2col 技巧:这是深度学习框架中最常用的卷积实现方法。核心思想是把卷积转化为矩阵乘法。

步骤

  1. 展开输入:对于每个输出位置 ,将对应的输入 patch(大小为 )展开成一个列向量。所有这些列向量组成矩阵

  2. 展开卷积核:将卷积核展开成一个行向量。

  3. 矩阵乘法4. 重塑输出:将结果重塑为输出特征图的形状。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import torch
import torch.nn.functional as F

def im2col_naive(x, kernel_size, stride=1, padding=0):
"""
将输入图像转换为列矩阵
x: (batch, channels, height, width)
"""
batch, channels, h, w = x.shape
kh, kw = kernel_size

# 添加 padding
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

# 提取所有 patch
col = torch.zeros(batch, channels * 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(batch, -1)

return col, out_h, out_w

def conv2d_via_im2col(x, weight, stride=1, padding=0):
"""
使用 im2col 实现 2D 卷积
x: (batch, in_channels, h, w)
weight: (out_channels, in_channels, kh, kw)
"""
batch = x.shape[0]
out_channels, in_channels, kh, kw = weight.shape

# im2col
col, out_h, out_w = im2col_naive(x, (kh, kw), stride, padding)

# 展开权重
weight_col = weight.reshape(out_channels, -1) # (out_channels, in_channels*kh*kw)

# 矩阵乘法
out = weight_col @ col # (batch, out_channels, out_{h*}out_w)

# 重塑输出
out = out.reshape(batch, out_channels, out_h, out_w)

return out

# 验证
x = torch.randn(2, 3, 8, 8)
weight = torch.randn(16, 3, 3, 3)

# 使用我们的实现
y1 = conv2d_via_im2col(x, weight, padding=1)

# 使用 PyTorch
y2 = F.conv2d(x, weight, padding=1)

print(f"差异: {(y1 - y2).abs().max().item():.6f}") # 应该接近 0

为什么用 im2col? 因为 GEMM(通用矩阵乘法)在 CPU 和 GPU 上都有高度优化的实现(如 cuBLAS)。虽然 im2col 会增加内存使用(输入被复制多次),但利用高效的 GEMM 带来的速度提升通常远超内存的开销。

转置卷积(反卷积)

转置卷积用于上采样,常见于生成模型和语义分割的解码器。

直觉:如果正向卷积将 的图像变为 (通常 ),转置卷积则做相反的事情。

矩阵视角:如果正向卷积可以表示为 是由卷积核构造的稀疏矩阵),转置卷积就是

注意:转置卷积不是卷积的逆运算。。它只是在矩阵维度上是"反过来"的。

1
2
3
4
5
6
7
8
9
10
11
# 转置卷积示例
conv = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1)
conv_transpose = nn.ConvTranspose2d(16, 3, kernel_size=3, stride=2, padding=1, output_padding=1)

x = torch.randn(1, 3, 32, 32)
y = conv(x) # (1, 16, 16, 16)
x_reconstructed = conv_transpose(y) # (1, 3, 32, 32)

print(f"输入形状: {x.shape}")
print(f"卷积后形状: {y.shape}")
print(f"转置卷积后形状: {x_reconstructed.shape}")

深度可分离卷积

标准卷积的参数量是 。深度可分离卷积将其分解为两步:

深度卷积( Depthwise):每个输入通道独立卷积,不混合通道。参数量:

逐点卷积( Pointwise) 卷积,混合通道。参数量:

总参数量:,远小于标准卷积。

矩阵视角:这是一种低秩分解。标准卷积的权重张量可以近似分解为深度和逐点两部分的乘积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 深度可分离卷积
class DepthwiseSeparableConv(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0):
super().__init__()
# 深度卷积: groups=in_channels 使每个通道独立
self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size,
stride=stride, padding=padding, groups=in_channels)
# 逐点卷积
self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1)

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

# 比较参数量
standard_conv = nn.Conv2d(64, 128, 3, padding=1)
depthwise_sep = DepthwiseSeparableConv(64, 128, 3, padding=1)

print(f"标准卷积参数量: {sum(p.numel() for p in standard_conv.parameters())}")
# 64 * 128 * 3 * 3 + 128 = 73856

print(f"深度可分离卷积参数量: {sum(p.numel() for p in depthwise_sep.parameters())}")
# 64 * 3 * 3 + 64 + 64 * 128 + 128 = 8896

Attention 机制中的矩阵运算

Attention 是现代深度学习最重要的创新之一。它的核心完全建立在矩阵运算之上。

注意力的直觉

想象你在图书馆找资料。你有一个问题( Query),图书馆有很多书( Key-Value 对)。你先用问题和每本书的关键词( Key)比对相关性,然后根据相关性加权获取书的内容( Value)。

Attention 做的事情:给定查询 ,计算它与所有键 的相似度,用这个相似度对值 加权求和。

缩放点积注意力

让我们拆解这个公式:

第一步——计算相似度矩阵

假设 个查询,每个 维), 个键)。

$$

QK^T ^{n m} $$ 就是第 个查询和第 个键的点积,代表它们的相似度。

第二步:除以 ——缩放

为什么要缩放?假设 的元素都是均值 0 、方差 1 的独立随机变量。它们的点积的方差是 。当 很大时,点积的值会很大,导致 softmax 饱和(梯度接近 0)。除以 使方差回到 1 。

第三步: softmax ——归一化

对每一行(每个查询)做 softmax,得到注意力权重。权重和为 1,可以解释为概率分布。

第四步:乘以 ——加权求和

假设 。注意力权重矩阵 ,输出

每个输出是所有值向量的加权平均,权重由注意力决定。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import torch
import torch.nn.functional as F
import math

def scaled_dot_product_attention(Q, K, V, mask=None):
"""
缩放点积注意力
Q: (batch, n_heads, seq_len_q, d_k)
K: (batch, n_heads, seq_len_k, d_k)
V: (batch, n_heads, seq_len_k, d_v)
mask: (batch, 1, 1, seq_len_k) 或 (batch, 1, seq_len_q, seq_len_k)
"""
d_k = Q.size(-1)

# 计算注意力分数
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
# scores: (batch, n_heads, seq_len_q, seq_len_k)

# 应用 mask(用于 decoder 的因果注意力)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))

# softmax 得到注意力权重
attn_weights = F.softmax(scores, dim=-1)

# 加权求和
output = torch.matmul(attn_weights, V)
# output: (batch, n_heads, seq_len_q, d_v)

return output, attn_weights

# 示例
batch, n_heads, seq_len, d_k = 2, 8, 10, 64
Q = torch.randn(batch, n_heads, seq_len, d_k)
K = torch.randn(batch, n_heads, seq_len, d_k)
V = torch.randn(batch, n_heads, seq_len, d_k)

output, weights = scaled_dot_product_attention(Q, K, V)
print(f"输出形状: {output.shape}") # (2, 8, 10, 64)
print(f"注意力权重形状: {weights.shape}") # (2, 8, 10, 10)
print(f"权重每行和: {weights[0, 0, 0].sum().item():.4f}") # 应该是 1

多头注意力

单一注意力只能学习一种"关注模式"。多头注意力让模型同时学习多种不同的关注模式。

其中每个头:

线性代数视角

  • 将输入投影到不同的子空间
  • 每个头在各自的子空间中计算注意力
  • 最后用 将所有头的输出混合

参数形状: - 输入维度 ,头数 ,每个头的维度 - Double exponent: use braces to clarifyW^Q, W^K, W^V ^{d_{model} d_{model}} (可以看作 矩阵的拼接) - Double exponent: use braces to clarifyW^O ^{d_{model} d_{model}}

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
assert d_model % n_heads == 0

self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // 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):
batch_size = Q.size(0)

# 线性投影并分头
# (batch, seq_len, d_model) -> (batch, seq_len, n_heads, d_k) -> (batch, n_heads, seq_len, d_k)
Q = self.W_q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = self.W_k(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = self.W_v(V).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)

# 注意力计算
attn_output, attn_weights = scaled_dot_product_attention(Q, K, V, mask)

# 合并多头
# (batch, n_heads, seq_len, d_k) -> (batch, seq_len, d_model)
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)

# 输出投影
output = self.W_o(attn_output)

return output, attn_weights

# 测试
mha = MultiHeadAttention(d_model=512, n_heads=8)
x = torch.randn(2, 10, 512) # (batch, seq_len, d_model)
output, weights = mha(x, x, x) # 自注意力: Q=K=V=x
print(f"输出形状: {output.shape}") # (2, 10, 512)

注意力的计算复杂度

标准注意力的时间和空间复杂度都是 ,其中 是序列长度。这是因为需要计算 的注意力矩阵。

对于长序列,这是主要瓶颈。各种高效注意力变体( Sparse Attention, Linear Attention, FlashAttention 等)都是为了解决这个问题。

FlashAttention 是一种算法优化,通过分块计算和减少 GPU 内存访问来加速标准注意力,不改变数学结果。

Transformer 的线性代数解读

Transformer 是现代 NLP 和多模态 AI 的基础架构。让我们从线性代数的角度完整解读它。

Transformer Encoder

一个 Encoder 层包含:

  1. 多头自注意力(已介绍)
  2. 前馈网络( FFN):两层全连接 + 激活函数
  3. 残差连接
  4. 层归一化

前馈网络

通常 $W_1 ^{d_{model} d_{ff}} W_2 ^{d_{ff} d_{model}} d_{ff} = 4 d_{model}$。

直觉: FFN 是逐位置的——对序列中每个位置独立应用相同的变换。它可以看作是一个两层的小型 MLP,先扩展维度、引入非线性,再压缩回原维度。

残差连接

残差连接让梯度可以"跳过"复杂的变换直接传回去,缓解梯度消失问题。

Transformer Decoder

Decoder 比 Encoder 多一个交叉注意力( Cross-Attention):

  • Query 来自 Decoder 的上一层输出
  • Key 和 Value 来自 Encoder 的输出

这让 Decoder 能够"看到"Encoder 处理的输入。

另外, Decoder 的自注意力是因果的( Causal):位置 只能注意到位置 。这通过注意力 mask 实现:

被 mask 的位置在 softmax 前被设为 ,使权重变为 0 。

1
2
3
4
5
6
7
8
9
10
11
12
def create_causal_mask(seq_len):
"""创建因果注意力 mask"""
mask = torch.tril(torch.ones(seq_len, seq_len))
return mask.unsqueeze(0).unsqueeze(0) # (1, 1, seq_len, seq_len)

mask = create_causal_mask(5)
print(mask[0, 0])
# tensor([[1., 0., 0., 0., 0.],
# [1., 1., 0., 0., 0.],
# [1., 1., 1., 0., 0.],
# [1., 1., 1., 1., 0.],
# [1., 1., 1., 1., 1.]])

位置编码

Transformer 没有循环结构,自注意力对位置是不敏感的( permutation equivariant)。位置编码给模型提供位置信息。

正弦位置编码

$$

PE_{pos, 2i} = ()

PE_{pos, 2i+1} = () $$

线性代数视角:每个位置被编码为一个 维向量。不同频率的正弦/余弦波使得模型可以学习相对位置关系—— 可以表示为 的线性函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()

pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))

pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)

pe = pe.unsqueeze(0) # (1, max_len, d_model)
self.register_buffer('pe', pe)

def forward(self, x):
# x: (batch, seq_len, d_model)
return x + self.pe[:, :x.size(1), :]

完整的 Transformer 层

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
class TransformerEncoderLayer(nn.Module):
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()

self.self_attn = MultiHeadAttention(d_model, n_heads)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(d_ff, d_model)
)

self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)

def forward(self, x, mask=None):
# 自注意力 + 残差
attn_output, _ = self.self_attn(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_output))

# 前馈网络 + 残差
ffn_output = self.ffn(x)
x = self.norm2(x + self.dropout(ffn_output))

return x

BatchNorm 和 LayerNorm

归一化层是深度学习中的关键组件。它们通过标准化激活值来稳定训练。

Batch Normalization

操作:对每个特征,在 batch 维度上计算均值和方差,然后标准化:

其中

标准化后,再用可学习参数缩放和平移:

$$

y_i = _i + $$

输入形状:对于卷积层 ,对每个通道 独立做 BN,均值和方差在 上计算。

问题: BN 依赖 batch 统计量。小 batch 时估计不准;推理时需要用训练时积累的移动平均。

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
28
29
class BatchNorm1d(nn.Module):
def __init__(self, num_features, eps=1e-5, momentum=0.1):
super().__init__()
self.eps = eps
self.momentum = momentum

# 可学习参数
self.gamma = nn.Parameter(torch.ones(num_features))
self.beta = nn.Parameter(torch.zeros(num_features))

# 运行时统计量(不参与梯度)
self.register_buffer('running_mean', torch.zeros(num_features))
self.register_buffer('running_var', torch.ones(num_features))

def forward(self, x):
# x: (batch, features)
if self.training:
mean = x.mean(dim=0)
var = x.var(dim=0, unbiased=False)

# 更新运行时统计量
self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * mean
self.running_var = (1 - self.momentum) * self.running_var + self.momentum * var
else:
mean = self.running_mean
var = self.running_var

x_norm = (x - mean) / torch.sqrt(var + self.eps)
return self.gamma * x_norm + self.beta

Layer Normalization

操作:对每个样本,在特征维度上计算均值和方差:

其中 是在同一个样本的所有特征上计算的。

输入形状:对于 的序列,对每个位置的 维向量独立做 LN 。

优点:不依赖 batch,适合小 batch 和序列模型( RNN 、 Transformer)。

矩阵视角的区别

  • BatchNorm:沿着 batch 维度(矩阵的行)标准化每个特征(列)
  • LayerNorm:沿着特征维度(矩阵的列)标准化每个样本(行)

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
28
29
30
31
class LayerNorm(nn.Module):
def __init__(self, normalized_shape, eps=1e-5):
super().__init__()
self.eps = eps

# 可学习参数
self.gamma = nn.Parameter(torch.ones(normalized_shape))
self.beta = nn.Parameter(torch.zeros(normalized_shape))

def forward(self, x):
# x: (..., normalized_shape)
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, keepdim=True, unbiased=False)

x_norm = (x - mean) / torch.sqrt(var + self.eps)
return self.gamma * x_norm + self.beta

# 比较
x = torch.randn(32, 10, 512) # (batch, seq_len, d_model)

bn = nn.BatchNorm1d(512) # 对 d_model 维度做 BN
ln = nn.LayerNorm(512) # 对 d_model 维度做 LN

# BN 需要 (batch, features, *) 的输入
x_bn = bn(x.transpose(1, 2)).transpose(1, 2)

# LN 直接处理
x_ln = ln(x)

print(f"BN 输出形状: {x_bn.shape}") # (32, 10, 512)
print(f"LN 输出形状: {x_ln.shape}") # (32, 10, 512)

RMSNorm

RMSNorm 是 LayerNorm 的简化版,只用均方根( RMS)标准化,不减均值:

优点:计算更简单,效果相近。 LLaMA 等模型使用 RMSNorm 。

1
2
3
4
5
6
7
8
9
class RMSNorm(nn.Module):
def __init__(self, dim, eps=1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))

def forward(self, x):
rms = torch.sqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
return self.weight * x / rms

参数高效微调: LoRA

大语言模型( LLM)有数十亿参数,全量微调成本很高。 LoRA( Low-Rank Adaptation)是一种高效的微调方法,其核心是低秩矩阵分解。

核心思想

预训练模型的权重为 。 LoRA 不直接修改 ,而是学习一个低秩的更新:

$$

W = W_0 + W = W_0 + BA $$

其中

参数量对比: - 全量微调: 个参数 - LoRA: 个参数

例如,对于 ,全量微调需要 1677 万参数。 LoRA 用 只需要 个参数,减少了 256 倍。

为什么低秩有效?

研究表明,预训练模型在微调时的权重变化具有低秩结构。也就是说, 的有效秩远小于其维度。 LoRA 直接约束 的秩为 ,相当于正则化。

直觉:微调是在预训练模型学到的特征空间上做小调整,不需要改变整个空间的结构。低秩更新只调整一个低维子空间。

实现

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class LoRALinear(nn.Module):
def __init__(self, base_layer, r=8, alpha=16, dropout=0.1):
"""
base_layer: 原始的 nn.Linear 层
r: LoRA 的秩
alpha: 缩放因子
"""
super().__init__()

self.base_layer = base_layer
self.r = r
self.alpha = alpha

in_features = base_layer.in_features
out_features = base_layer.out_features

# LoRA 参数
self.lora_A = nn.Parameter(torch.randn(r, in_features) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_features, r))

self.scaling = alpha / r
self.dropout = nn.Dropout(dropout)

# 冻结原始权重
for param in base_layer.parameters():
param.requires_grad = False

def forward(self, x):
# 原始输出
base_output = self.base_layer(x)

# LoRA 输出
# x: (..., in_features)
# lora_A: (r, in_features)
# lora_B: (out_features, r)
lora_output = self.dropout(x) @ self.lora_A.T @ self.lora_B.T * self.scaling

return base_output + lora_output

def merge_weights(self):
"""将 LoRA 权重合并到原始权重,推理时使用"""
self.base_layer.weight.data += (self.lora_B @ self.lora_A) * self.scaling

# 使用示例
base = nn.Linear(4096, 4096)
lora = LoRALinear(base, r=8)

print(f"原始参数量: {sum(p.numel() for p in base.parameters())}") # 16781312
print(f"LoRA 可训练参数量: {sum(p.numel() for p in lora.parameters() if p.requires_grad)}") # 65536

x = torch.randn(2, 10, 4096)
y = lora(x)
print(f"输出形状: {y.shape}") # (2, 10, 4096)

LoRA 在 Transformer 中的应用

通常对注意力层的 应用 LoRA 。这些是信息流动的关键位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def apply_lora_to_attention(attention_module, r=8, alpha=16):
"""给 MultiHeadAttention 添加 LoRA"""
attention_module.W_q = LoRALinear(attention_module.W_q, r=r, alpha=alpha)
attention_module.W_k = LoRALinear(attention_module.W_k, r=r, alpha=alpha)
attention_module.W_v = LoRALinear(attention_module.W_v, r=r, alpha=alpha)
attention_module.W_o = LoRALinear(attention_module.W_o, r=r, alpha=alpha)
return attention_module

# 应用到 Transformer
mha = MultiHeadAttention(d_model=512, n_heads=8)
mha_with_lora = apply_lora_to_attention(mha, r=8)

total_params = sum(p.numel() for p in mha_with_lora.parameters())
trainable_params = sum(p.numel() for p in mha_with_lora.parameters() if p.requires_grad)
print(f"总参数量: {total_params}")
print(f"可训练参数量: {trainable_params}")
print(f"可训练比例: {trainable_params / total_params * 100:.2f}%")

QLoRA 和其他变体

QLoRA:结合量化和 LoRA 。基础模型用 4 位量化存储, LoRA 参数用 FP16/BF16,进一步减少内存。

DoRA:将权重分解为方向和幅度两部分,分别用 LoRA 适配,效果更好。

AdaLoRA:自适应分配不同层的秩 ,重要的层用更高的秩。

深度学习优化中的线性代数

权重初始化

好的初始化对训练至关重要。目标是让前向传播和反向传播时信号不会爆炸或消失。

Xavier 初始化(适用于 tanh/sigmoid):

$$

W U(-, ) $$

或者高斯版本:

$$

W N(0, ) $$

推导思路:假设输入 的方差为 ,我们希望输出 的方差也是 。这要求 。同理,反向传播要求 。取两者的平均。

He 初始化(适用于 ReLU):

$$

W N(0, ) $$

ReLU 会"杀死"一半的激活(负数变 0),所以方差要乘 2 补偿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def xavier_init(shape):
n_in, n_out = shape
std = (2 / (n_in + n_out)) ** 0.5
return torch.randn(shape) * std

def he_init(shape):
n_in, n_out = shape
std = (2 / n_in) ** 0.5
return torch.randn(shape) * std

# 验证方差保持
n_in, n_out = 1000, 1000
W = he_init((n_out, n_in))
x = torch.randn(100, n_in) # 100 个样本

y = x @ W.T
print(f"输入方差: {x.var().item():.4f}")
print(f"输出方差: {y.var().item():.4f}") # 应该接近输入方差

梯度问题与奇异值

深层网络中,梯度需要经过很多层传播。如果每层的雅可比矩阵的奇异值都大于 1,梯度会爆炸;都小于 1,梯度会消失。

设网络为 ,反向传播的梯度:

梯度的范数大致是各层雅可比矩阵奇异值的乘积。

解决方案

  1. 残差连接,雅可比矩阵变为 ,特征值围绕 1
  2. 归一化:控制激活值的范围
  3. 梯度裁剪

优化器的矩阵视角

SGD:梯度下降

SGD with Momentum:累积梯度

$$

v_{t+1} = v_t + L(t) {t+1} = t - v{t+1} $$

Adam:自适应学习率

$$

m_t = 1 m{t-1} + (1 - _1) g_t

v_t = 2 v{t-1} + (1 - _2) g_t^2 _t = , t = {t+1} = _t - $$

线性代数视角: Adam 相当于用对角预条件矩阵 缩放梯度。这近似于二阶方法(牛顿法用 Hessian 的逆, Adam 用对角近似)。

练习题

基础题

1. 证明:对于线性层 ,如果 是正交矩阵(),则前向传播和反向传播都不会改变向量的范数。

2. 假设输入 ,经过三层全连接网络:。写出每层权重矩阵的形状,并计算总参数量。

3. 解释为什么多头注意力中,每个头的维度通常设为 $ d_{model} / h h d_{model}$。

4. BatchNorm 和 LayerNorm 分别在哪些维度上计算均值和方差?对于形状为 的卷积特征图,它们的归一化统计量形状分别是什么?

进阶题

5. 推导缩放点积注意力中除以 的必要性。假设 的每个元素独立同分布于 ,计算 的每个元素的均值和方差。

6. 对于 im2col 方法: - 假设输入 ,卷积核 , stride=1, padding=1 - 计算 im2col 后矩阵的形状 - 分析这种方法的内存开销

7. 证明:如果 ),则

8. 分析 ResNet 残差块 的梯度流。证明反向传播时存在一条"shortcut"路径,梯度可以不经过 直接传到前面的层。

编程题

9. 实现一个完整的 Transformer Encoder(包含多层),并用它处理一个简单的序列分类任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 骨架代码
class TransformerEncoder(nn.Module):
def __init__(self, d_model, n_heads, d_ff, n_layers, dropout=0.1):
super().__init__()
# TODO: 实现
pass

def forward(self, x, mask=None):
# TODO: 实现
pass

# 测试
encoder = TransformerEncoder(d_model=256, n_heads=4, d_ff=1024, n_layers=4)
x = torch.randn(2, 20, 256) # (batch, seq_len, d_model)
output = encoder(x)
print(f"输出形状: {output.shape}") # 应该是 (2, 20, 256)

10. 实现一个简化的 LoRA 训练流程: - 加载一个预训练的小型模型(如一个简单的分类器) - 对其线性层应用 LoRA - 在新任务上微调 LoRA 参数 - 比较全量微调和 LoRA 微调的参数量和效果

11. 实现并可视化不同归一化方法的效果: - 生成一个随机 batch - 分别应用 BatchNorm 、 LayerNorm 、 RMSNorm - 可视化归一化前后特征分布的变化

12. 分析一个实际的 Transformer 模型(如 BERT-tiny 或 GPT-2-small)的计算复杂度: - 统计不同组件(注意力、 FFN 、归一化等)的 FLOPs 占比 - 分析序列长度对计算量的影响 - 绘制计算量随序列长度变化的曲线

思考题

13. 为什么 Transformer 中普遍使用 LayerNorm 而不是 BatchNorm?从训练稳定性、序列长度变化、并行计算等角度分析。

14. LoRA 假设微调时的权重变化是低秩的。在什么情况下这个假设可能不成立?如何检测?

15. 如果要将传统的 2D CNN 迁移到处理 3D 数据(如视频或医学影像),从矩阵运算的角度分析计算量会如何变化。

本章总结

深度学习的核心操作都可以用线性代数来描述:

  • 全连接层:矩阵乘法 + 非线性激活
  • 反向传播:链式法则的矩阵形式,梯度通过转置矩阵传播
  • 卷积:可以通过 im2col 转化为矩阵乘法,利用高效 GEMM 加速
  • 注意力机制:查询-键点积计算相似度,对值加权求和
  • Transformer:注意力 + FFN + 残差 + 归一化的组合
  • 归一化层:在不同维度上标准化,稳定训练
  • LoRA:低秩矩阵分解实现高效微调

理解这些线性代数基础,能让你: 1. 更好地理解模型的工作原理 2. 高效地实现和优化模型 3. 设计新的网络架构 4. 调试训练中的问题(梯度消失/爆炸等)

参考资料

  1. Vaswani, A., et al. "Attention is All You Need." NeurIPS 2017.
  2. Hu, E., et al. "LoRA: Low-Rank Adaptation of Large Language Models." ICLR 2022.
  3. Ba, J., Kiros, J., & Hinton, G. "Layer Normalization." arXiv 2016.
  4. Ioffe, S., & Szegedy, C. "Batch Normalization." ICML 2015.
  5. He, K., et al. "Deep Residual Learning for Image Recognition." CVPR 2016.
  6. Glorot, X., & Bengio, Y. "Understanding the difficulty of training deep feedforward neural networks." AISTATS 2010.

本文是《线性代数的本质与应用》系列的第十六章,共 18 章。

  • 本文标题:线性代数(十六)深度学习中的线性代数
  • 本文作者:Chen Kai
  • 创建时间:2019-03-22 14:30:00
  • 本文链接:https://www.chenk.top/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%EF%BC%88%E5%8D%81%E5%85%AD%EF%BC%89%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E4%B8%AD%E7%9A%84%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论