
LSTM & GRU —— 门控循环神经网络
系统讲解LSTM与GRU的门控机制设计哲学与数学原理。从LSTM的“双轨”架构(细胞状态+隐藏状态)与三门结构(遗忘门、输入门、输出门)出发,推导细胞状态更新公式如何通过按元素加法路径缓解梯度消失,并深入剖析GRU的两大简化及其参数量优势。附带讨论双向LSTM与深层堆叠LSTM的应用场景。
阅读文章ZHY's Blog
A UNIVERSE OF IDEAS · BY ZHANG HAOYI
让好奇心 点亮知识宇宙
在代码、模型与思想之间自由漫游。这里持续记录人工智能、机器学习、软件工程与成长实践,让每次阅读都成为一次新的发现。
ARTICLE NOTE
欢迎来到深度学习的世界。在接触自动微分、MLP或优化器之前,我们必须先厘清数据在计算机中的物理载体——张量(Tensor)。
本系列将本博客置于开篇之首,是后续一切复杂模型的“刚需”。无论是后续全连接层的特征混合(Wx+b),卷积核在图像上的滑动,还是循环状态在时序上的传递,其底层本质都归结于高效的张量变换。若在手推反向传播时被“维度不匹配”折磨,或理解权重初始化时对矩阵形状生疏,根源往往在于本篇基础尚未夯实。
本文不满足于“高维数组”的模糊直觉,而是直击五个根本痛点:其一,厘清reshape与transpose背后 “存储与视图”的分离思想,这是高效内存管理的前提;其二,严格区分Hadamard积(*)与矩阵乘法(@) —— 前者是注意力与门控机制的“调制器”,后者是全连接层的“特征混合器”;其三,解析L1/L2/Frobenius范数的几何意义,这将是理解后续权重衰减(正则化)的数学基石;其四,直观拆解特征分解 “换基-缩放-换回” 的几何过程;其五,揭示SVD作为任意矩阵降维与压缩的终极价值。
需要特别留意的是,本篇对线性变换的空间感知,将直接服务于卷积神经网络中感受野的推导。请带着“数据如何流动”的疑问进入正文——唯有基座稳固,后续的模型大厦方能拔地而起。
张量(Tensor)是深度学习中数据的基本容器。你可以把它理解为多维数组的统称:
3.14[1, 2, 3]张量的形状(Shape) 是一个元组,描述了每个维度的大小。例如 shape=(3, 4) 表示一个3行4列的矩阵。
1import torch2import numpy as np3
4# 创建各种张量5scalar = torch.tensor(3.14) # 0阶:标量6vector = torch.tensor([1, 2, 3]) # 1阶:向量7matrix = torch.tensor([[1, 2], [3, 4]]) # 2阶:矩阵8tensor_3d = torch.randn(2, 3, 4) # 3阶:形状(2,3,4)9
10print(f"标量形状: {scalar.shape}") # torch.Size([])11print(f"向量形状: {vector.shape}") # torch.Size([3])12print(f"矩阵形状: {matrix.shape}") # torch.Size([2, 2])13print(f"3D张量形状: {tensor_3d.shape}") # torch.Size([2, 3, 4])形状变换是指在不改变张量数据内容的情况下,改变其维度和每个维度的大小。理解这些操作的关键,在于搞清楚存储(Storage) 与视图(View) 的区别:
1# 创建一个连续存储的张量2x = torch.arange(12)3print(f"原始数据: {x}") # tensor([0, 1, 2, ..., 11])4
5# reshape:改变视图,数据在内存中的顺序不变6x_reshaped = x.reshape(3, 4)7print(f"reshape后:\n{x_reshaped}")8# tensor([[ 0, 1, 2, 3],9# [ 4, 5, 6, 7],10# [ 8, 9, 10, 11]])reshape —— 重塑形状reshape是最常用的形状变换函数,它不改变元素数量和元素值,只改变张量的逻辑形状。
1# 将24个元素重塑为不同形状2x = torch.arange(24)3
4# 变成 2×3×45a = x.reshape(2, 3, 4)6print(a.shape) # torch.Size([2, 3, 4])7
8# 使用 -1 让PyTorch自动计算该维度大小9b = x.reshape(2, 3, -1)10print(b.shape) # torch.Size([2, 3, 4]),-1自动算为411
12# 展平为一维13c = x.reshape(-1)14print(c.shape) # torch.Size([24])关键点:reshape要求新形状的元素总数与原张量相同。-1是一个便利写法,表示“自动计算这个维度的大小”。
transpose 与 permute —— 交换维度transpose用于交换两个指定的维度,permute可以重新排列任意数量的维度。
1# 创建一个3维张量2x = torch.randn(2, 3, 4) # shape: (2, 3, 4)3
4# transpose:交换维度0和维度15y = x.transpose(0, 1)6print(y.shape) # torch.Size([3, 2, 4])7
8# permute:重新排列所有维度9z = x.permute(2, 0, 1)10print(z.shape) # torch.Size([4, 2, 3])重要区别:reshape只是改变“看待数据的方式”,而transpose/permute会改变维度之间的逻辑关系。经过transpose后,张量在内存中变得不连续(non-contiguous) ,此时如果再调用reshape,PyTorch会先拷贝数据使其连续。
1x = torch.arange(12).reshape(3, 4)2print(x)3# tensor([[ 0, 1, 2, 3],4# [ 4, 5, 6, 7],5# [ 8, 9, 10, 11]])6
7# transpose后,内存布局改变了8x_t = x.transpose(0, 1)9print(x_t)10# tensor([[ 0, 4, 8],11# [ 1, 5, 9],12# [ 2, 6, 10],13# [ 3, 7, 11]])14
15print(x_t.is_contiguous()) # Falsesqueeze 与 unsqueeze —— 压缩与扩展维度squeeze:删除所有大小为1的维度unsqueeze:在指定位置插入一个大小为1的维度1x = torch.randn(1, 3, 1, 4)2
3# squeeze:删除所有尺寸为1的维度4y = x.squeeze()5print(y.shape) # torch.Size([3, 4])6
7# unsqueeze:在指定位置插入维度8z = torch.tensor([1, 2, 3])9w = z.unsqueeze(0) # 在第0维插入10print(w.shape) # torch.Size([1, 3])在计算机视觉中,图像数据经常需要在不同形状间转换:
1# 假设有一批RGB图像:batch_size=32, 通道数=3, 高=224, 宽=2242images = torch.randn(32, 3, 224, 224)3
4# 展平为特征向量(用于全连接层)5flattened = images.reshape(32, -1)6print(flattened.shape) # torch.Size([32, 150528])7
8# 交换通道维度和宽高维度(某些框架需要)9# 从 (B, C, H, W) 变为 (B, H, W, C)10images_chw = images.permute(0, 2, 3, 1)11print(images_chw.shape) # torch.Size([32, 224, 224, 3])@)与Hadamard积(*)的区别Hadamard积(Hadamard Product) ,也叫逐元素乘法(Element-wise Multiplication) 或Schur积,是两个同型矩阵对应位置元素相乘:
(A∘B)ij=aij⋅bij
在Python中,*运算符默认执行的就是Hadamard积。
1import numpy as np2
3A = np.array([[1, 2], [3, 4]])4B = np.array([[5, 6], [7, 8]])5
6# Hadamard积(逐元素乘法)7C = A * B8print("Hadamard积 (A * B):")9print(C)10# [[ 5 12]11# [21 32]]12# 验证:1×5=5, 2×6=12, 3×7=21, 4×8=32Hadamard积具有交换律和结合律,这是它与普通矩阵乘法的关键区别之一。
矩阵乘法(也叫点积、matmul)遵循“左行乘右列”的规则:
(AB)ij=∑k=1naik⋅bkj
要求:A的列数 = B的行数。结果的形状为 (A的行数, B的列数)。
1# 矩阵乘法(使用 @ 运算符)2D = A @ B3print("矩阵乘法 (A @ B):")4print(D)5# [[19 22]6# [43 50]]7# 验证:19 = 1×5 + 2×7, 22 = 1×6 + 2×88# 43 = 3×5 + 4×7, 50 = 3×6 + 4×89
10# 形状规则11A2 = np.ones((2, 3))12B2 = np.ones((3, 4))13C2 = A2 @ B214print(C2.shape) # (2, 4)在Python中,矩阵乘法可以通过三种方式实现:
@ 运算符(Python 3.5+引入,最推荐)np.matmul() 函数np.dot() 函数(对二维数组等同于矩阵乘法,但对高维数组行为不同)| 特性 | Hadamard积(*) | 矩阵乘法(@) |
|---|---|---|
| 运算规则 | 对应元素相乘 | 行×列求和 |
| 形状要求 | 必须完全相同 | A的列数 = B的行数 |
| 结果形状 | 与原矩阵相同 | (A的行数, B的列数) |
| 交换律 | ✅ 成立 | ❌ 不成立 |
| AI中的典型用途 | 注意力权重、门控机制 | 全连接层、卷积层 |
1# 直观对比2A = np.array([[1, 2], [3, 4]])3B = np.array([[5, 6], [7, 8]])4
5print("A * B (Hadamard):")6print(A * B)7# [[ 5 12]8# [21 32]]9
10print("A @ B (矩阵乘法):")11print(A @ B)12# [[19 22]13# [43 50]]在神经网络中,全连接层 y = Wx + b 使用的是矩阵乘法(@),而注意力机制中的权重计算、门控循环单元中的门控信号则常使用Hadamard积(*)。
范数(Norm) 的本质是度量向量或矩阵“大小”的工具。不同的范数从不同角度衡量“大小”,在机器学习中各有用途。
L1范数定义为向量各元素绝对值之和:
∥x∥1=∑i=1n∣xi∣
几何意义:L1范数对应曼哈顿距离(Manhattan Distance) ——就像在曼哈顿街区行走,只能沿着街道直角转弯,距离是各段路程的绝对值之和。
1import torch2
3v = torch.tensor([3.0, -4.0])4
5# L1范数 = |3| + |-4| = 76l1_norm = torch.linalg.norm(v, ord=1)7print(f"L1范数: {l1_norm.item()}") # 7.0为什么L1范数能产生稀疏解? 在二维平面上,L1范数的“单位球”是一个菱形(顶点在坐标轴上)。当用L1范数做正则化时,优化问题的解更容易落在坐标轴上——这意味着某些维度的权重被压缩为0,从而实现特征选择。这就是Lasso回归使用L1正则化的原因。
L2范数定义为向量各元素平方和的平方根:
∥x∥2=∑i=1nxi2
几何意义:L2范数就是欧几里得距离(Euclidean Distance) ——从原点出发到目标点的直线距离。
1# L2范数 = sqrt(3² + (-4)²) = 52l2_norm = torch.linalg.norm(v, ord=2)3print(f"L2范数: {l2_norm.item()}") # 5.0L2范数的平方有一个常用形式:∥x∥22=xTx。在机器学习中,L2正则化(岭回归/权重衰减)就是惩罚权重的L2范数平方,倾向于让所有权重都较小但非零,从而防止过拟合。
Frobenius范数是L2范数在矩阵上的推广——将矩阵所有元素的平方和开根号:
∥A∥F=∑i=1m∑j=1n∣aij∣2
从另一个角度看,如果把矩阵的所有列“堆叠”成一个长向量,Frobenius范数就是这个长向量的L2范数。
1M = torch.tensor([[1.0, 2.0], [3.0, 4.0]])2
3# Frobenius范数 = sqrt(1² + 2² + 3² + 4²) = sqrt(30) ≈ 5.4774fro_norm = torch.linalg.norm(M, ord='fro')5print(f"Frobenius范数: {fro_norm.item():.3f}") # 5.477Frobenius范数的几何意义:它衡量的是矩阵作为一个整体在“欧几里得空间”中的长度。在矩阵低秩近似中(如SVD截断),我们通常用Frobenius范数来衡量重建误差——保留的奇异值越多,重建误差(Frobenius范数)越小。
| 范数 | 公式 | 几何意义 | 机器学习用途 |
|---|---|---|---|
| L1 | $\sum | x_i | $ |
| L2 | ∑xi2 | 欧几里得距离 | 岭回归、权重衰减 |
| Frobenius | ∑aij2 | 矩阵的“长度” | 矩阵近似、SVD重建误差 |
特征分解(Eigendecomposition)是将一个方阵分解为特征向量和特征值的操作。
核心思想:一个矩阵 A 代表一个线性变换。绝大多数向量经过这个变换后,方向和长度都会改变。但存在一些特殊的向量,它们的方向在变换后保持不变,只是长度被拉伸或压缩了。
这些特殊的向量就是特征向量(Eigenvector) ,拉伸或压缩的倍数就是特征值(Eigenvalue) :
Av=λv
其中 v 是特征向量,λ 是特征值。
如果一个 n×n 的方阵 A 有 n 个线性无关的特征向量,它可以被分解为:
A=QΛQ−1
其中:
想象一个线性变换 A 作用于空间中的向量:
1import numpy as np2
3# 创建一个对称矩阵(保证可特征分解)4A = np.array([[4, 1], [1, 3]])5
6# 特征分解7eigenvalues, eigenvectors = np.linalg.eig(A)8
9print("特征值:", eigenvalues) # [4.618, 2.382]10print("特征向量:\n", eigenvectors)11# 每一列是一个特征向量12
13# 验证:A @ v = λ * v14v = eigenvectors[:, 0]15lam = eigenvalues[0]16print("A @ v:", A @ v)17print("λ * v:", lam * v)18# 两者应该相等特征值的大小表示“重要性” :特征值越大,对应的特征向量方向在变换中越重要。这就是为什么PCA(主成分分析)会选择最大特征值对应的特征向量作为主成分——它们代表了数据中方差最大的方向。
特征分解要求矩阵必须是方阵。但在实际数据中,我们经常遇到非方阵的数据矩阵(如 m 个样本 × n 个特征,m=n)。这时候就需要奇异值分解(SVD) 出场了。
特征值分解的局限:只有方阵才能做特征分解。
SVD的突破:任意矩阵(无论是否方阵)都可以进行奇异值分解。
任意 m×n 的矩阵 A 都可以分解为:
A=UΣVT
其中:
1from scipy.linalg import svd2
3# 任意非方阵矩阵4A = np.array([[1, 2, 3],5 [4, 5, 6],6 [7, 8, 9],7 [10, 11, 12]]) # shape: (4, 3)8
9U, S, Vt = svd(A)10
11print("U的形状:", U.shape) # (4, 4)12print("S(奇异值):", S) # [25.46, 1.72, 0.00]13print("Vt的形状:", Vt.shape) # (3, 3)从几何角度看,任何矩阵 A 的线性变换效果都可以分解为三个步骤:
奇异值 σi 就是第 i 个方向上的缩放倍数。奇异值越大,说明这个方向在变换中越重要。
SVD最强大的应用在于低秩近似(Low-Rank Approximation) ——用少数几个最大的奇异值及其对应的奇异向量来近似原始矩阵。
A≈Ak=∑i=1kσiuiviT
其中 k 远小于矩阵的秩。保留的奇异值越多,近似越精确;保留的越少,压缩率越高。
这是理解SVD数据压缩能力最直观的例子。
1import numpy as np2import matplotlib.pyplot as plt3from scipy.linalg import svd4from PIL import Image5
6# 加载灰度图像并转为矩阵7# 这里用随机数据模拟,实际使用时替换为真实图像8# img = Image.open('image.jpg').convert('L')9# A = np.array(img, dtype=float)10
11# 为演示,创建一个有结构的矩阵(模拟图像)12np.random.seed(42)13A = np.random.randn(200, 200)14# 加一些结构使其更像图像15A = A @ A.T # 使其具有低秩结构16
17def svd_compress(A, k):18 """用SVD将矩阵A压缩到k个奇异值"""19 U, S, Vt = svd(A, full_matrices=False)20 # 只保留前k个奇异值21 S_k = np.diag(S[:k])22 U_k = U[:, :k]23 Vt_k = Vt[:k, :]24 # 重建矩阵25 A_k = U_k @ S_k @ Vt_k26 return A_k, S27
28# 分别用不同数量的奇异值重建29ks = [5, 20, 50, 100, 200]30fig, axes = plt.subplots(1, len(ks), figsize=(15, 3))31
32for i, k in enumerate(ks):33 A_k, S = svd_compress(A, k)34 axes[i].imshow(A_k, cmap='gray')35 axes[i].set_title(f'k={k}')36 axes[i].axis('off')37
38plt.tight_layout()39plt.show()40
41# 计算压缩比42def compression_ratio(m, n, k):43 """SVD压缩的存储压缩比"""44 # 原始存储: m*n45 # 压缩后存储: U(m*k) + Sigma(k) + Vt(k*n)46 compressed = m * k + k + k * n47 original = m * n48 return compressed / original49
50print(f"原始大小: {200*200} = 40000")51print(f"k=20时压缩后大小: {compression_ratio(200, 200, 20):.2%}")52# 输出类似: 19.90% —— 只需约20%的存储空间!SVD图像压缩的直观理解:
实际案例中,保留前100个奇异值(k=100)重建的图像与原图几乎无法区分,但存储空间只需要约12%。
| 特性 | 特征分解(EVD) | 奇异值分解(SVD) |
|---|---|---|
| 适用矩阵 | 仅限方阵 | 任意矩阵 |
| 分解结果 | A=QΛQ−1 | A=UΣVT |
| 正交性 | 特征向量不一定正交 | 奇异向量一定正交 |
| 数值稳定性 | 对病态矩阵敏感 | 数值稳定性更好 |
| PCA实现 | 对协方差矩阵做EVD | 直接对数据矩阵做SVD |
在实际工程中,PCA通常用SVD实现而非特征分解,因为SVD直接作用于数据矩阵,数值更稳定,且适用于大规模数据。
回顾本文的核心脉络:
张量是AI数据的通用容器,理解shape、reshape、transpose、squeeze等操作的本质——存储与视图的分离——是高效处理数据的基础。
矩阵乘法(@) 与Hadamard积(*) 服务于完全不同的目的:前者是“特征混合”(全连接层),后者是“特征调制”(注意力机制)。
范数是衡量“大小”的尺子:L1是曼哈顿距离(产生稀疏),L2是欧几里得距离(权重衰减),Frobenius是矩阵的“L2长度”(衡量重建误差)。
特征分解将方阵分解为“方向×重要性”,是理解线性变换“主轴”的窗口。
SVD是特征分解的终极推广——适用于任意矩阵,是降维(PCA)、数据压缩(图像压缩)、推荐系统的数学基石。
这些概念不是孤立的数学公式,而是贯穿AI模型设计、训练、部署全流程的思维工具。理解它们,你就能真正“读懂”深度学习模型的每一个运算。
按顺序完成这组文章,循序渐进地掌握主题
发现错误、内容过时或有改进想法?欢迎告诉我
根据本文分类与标签,为你推荐可能感兴趣的内容

系统讲解LSTM与GRU的门控机制设计哲学与数学原理。从LSTM的“双轨”架构(细胞状态+隐藏状态)与三门结构(遗忘门、输入门、输出门)出发,推导细胞状态更新公式如何通过按元素加法路径缓解梯度消失,并深入剖析GRU的两大简化及其参数量优势。附带讨论双向LSTM与深层堆叠LSTM的应用场景。
阅读文章
系统讲解循环神经网络(RNN)的数学原理与随时间反向传播(BPTT)算法。从RNN的循环结构与共享参数出发,推导BPTT的梯度表达式,揭示梯度消失与梯度爆炸的数学根源,并介绍梯度裁剪作为应对梯度爆炸的工程解法。附带讨论LSTM如何通过门控机制缓解梯度消失。
阅读文章
系统讲解ResNet残差网络的设计哲学与数学原理:从退化问题的本质出发,推导残差块F(x)+x如何通过恒等映射直通路径缓解梯度消失,详解Bottleneck块如何将参数量减少约94%,并对比ResNet(加法融合)与DenseNet(拼接融合)的梯度流动差异。
阅读文章请使用微信扫描二维码分享
当前文章会保持在原页面