资讯动态

深度学习数学:从张量形状到反向传播的工程实践

发布时间:2026/10/27 2:14:16 来源:尧图企业网站定制
简介《Math for Deep Learning》是No Starch Press于2022年出版的深度学习数学指南作者Ronald T. Kneusel自2003年起从事工业界机器学习工作并取得机器学习博士学位内容面向希望读懂神经网络内部机制、补齐数学短板的实践者。全书围绕线性代数、概率论与统计、微积分和优化算法展开把向量与矩阵运算、数据分布与噪声评估、梯度下降等概念对应到CNN、RNN、LSTM等模型的工作原理上帮助读者从“会调库”走向“理解为什么”。资源包为单一PDF文件大小7.71MB共1个文件保留原书排版便于在电脑或平板上按章节检索阅读标签中的Python提示读者可结合NumPy、SciPy、TensorFlow、PyTorch等生态将公式与代码实现相互印证。目前已有83人学习下载适合具备基础编程能力、准备系统补充数学的深度学习入门者与转型开发者。1. 先修形状再修公式深度学习数学真正卡人的地方很多人以为深度学习数学的门槛在公式其实真正卡人的是两个更朴素的东西形状和记账。用 Python 把这些数学写成数组操作时把(32, 128)和(128, 64)乘在一起没报错模型却默默学不动手推反向传播时每一行推导都对梯度就是跟数值差分对不上小数点后第三位。这两个坑都不是线性代数或微积分没学明白而是没人告诉你深度学习里的数学是被数据流串起来的概率定义损失和不确性线性代数定义形状与变换微积分决定参数往哪走Python 只是把这四件事落成可执行的数组操作。这份 2022 年的深度学习数学读物价值恰恰在于它的章节顺序就是这条线索先花一章把数组、数据类型、索引这些“工程地基”铺好再上概率、统计、线性代数、微分与矩阵微积分最后三章直接落到神经网络的数据流、反向传播和梯度下降。它不教你调用框架而是让你在框架报错或 loss 不降时能顺着数学往回查到哪一层出了偏差。下面按机制层次拆开讲能跑的地方都给可复现的代码。2. 从张量到 SVD线性代数怎么把矩阵拆开又拼回去2.1 阶数决定运算形状本身就是一种约束标量、向量、矩阵、张量这四个词听起来像分类游戏实际决定的是“这个量能参与哪些运算”。0 阶没有轴1 阶有一个轴2 阶有两个轴n 阶有 n 个轴两个数组能不能相加取决于从尾部对齐后每一维是否相等或其中之一为 1。深度学习中把 batch 放在第 0 维不是随意的它让“对每个样本做同样的变换”这件事变成一次矩阵乘法而卷积网络里(N, C, H, W)的排布则让同一层的所有通道共享卷积核参数。import numpy as np # 0 阶到 4 阶形状携带了「这个量参与什么运算」的信息 scalar np.array(3.0) # shape() 0 阶 vector np.arange(4, dtypenp.float32) # shape(4,) 1 阶 matrix np.zeros((3, 4)) # shape(3,4) 2 阶 tensor np.zeros((8, 3, 32, 32)) # (N,C,H,W) 4 阶 # dtype 不是装饰float32 与 float64 的累加误差量级不同 print(vector.dtype, vector.nbytes / vector.size) # float32 4.0 print(matrix.shape, matrix.ndim, matrix.size) # (3, 4) 2 12ndim是阶数size是元素总数nbytes/size得到单元素字节数。这段代码的意义在于建立一个习惯写任何模型代码前先问自己“这个量的阶数和每一维语义是什么”。混合精度训练里权重用 16 位存、累加用 32 位做本质上就是在 dtype 上做取舍——低精度省显存和带宽但梯度累加如果也用低精度小量会被大数“吃掉”训练后期更新直接停在原地。2.2 矩阵乘法为什么是行乘列初学者最常问的问题是既然两个矩阵逐元素相乘也有定义为什么深度学习里说的乘法偏偏是行乘列原因是矩阵在这里不是数据表而是线性变换。B把 n 维向量映到 k 维A再把 k 维映到 m 维复合起来的变换就对应AB而它的第 i 行第 j 列恰好等于 A 的第 i 行与 B 的第 j 列的内积。这个定义不是约定俗成是“变换的复合”逼出来的唯一形式。import numpy as np rng np.random.default_rng(0) A rng.normal(size(2, 3)) B rng.normal(size(3, 4)) def matmul_naive(A, B): m, k A.shape k2, n B.shape assert k k2, f内维不匹配: {A.shape} {B.shape} C np.zeros((m, n)) for i in range(m): for j in range(n): s 0.0 for t in range(k): s A[i, t] * B[t, j] # 行 i 与列 j 的内积 C[i, j] s return C print(np.allclose(matmul_naive(A, B), A B)) # True print(np.allclose(A B, np.einsum(ik,kj-ij, A, B))) # Truem、k、n分别是左矩阵行数、内维、右矩阵列数内维不相等就必须报错而不是悄悄广播。三重循环的复杂度是 O(mkn)GPU 上做的是同一件事只是把输出切成小块tile塞进片上存储反复复用再配合乘加融合指令并行einsum的写法把“对 k 求和”这个动作显式写出来读别人代码时比一串更好核对维度。2.3 特征分解为什么矩阵乘向量只缩放不转向特征向量回答的是一个很具体的问题存不存在某个方向矩阵作用在它身上只改变长度、不改变方向把Av λv移项得(A - λI)v 0要让非零 v 存在A - λI必须不可逆也就是行列式为 0解这个多项式就得到特征值。对称矩阵有更强的性质特征值全是实数特征向量两两正交可以把A写成QΛQᵀ这就是谱定理。协方差矩阵天然对称所以后面 PCA 能做得那么干净靠的就是这条性质。import numpy as np rng np.random.default_rng(1) M rng.normal(size(4, 4)) S M M.T # 对称半正定类似协方差矩阵 w, V np.linalg.eigh(S) # 对称专用返回升序特征值 print(w) # 全部非负 print(np.allclose(S V, V * w)) # S v_i lambda_i v_i 逐列成立 print(np.allclose(V.T V, np.eye(4)))# 特征向量正交归一 print(np.linalg.cond(S)) # 条件数 最大/最小特征值eigh只用于对称矩阵返回的特征值按升序排列用eig会得到复数结果并且顺序不定这是很多人第一次用错的点。条件数λmax/λmin是理解训练难度的关键条件数越大损失曲面越像一条狭长的山谷梯度下降会在两侧来回振荡、沿谷底爬得很慢。2.4 SVD 与伪逆不满秩的矩阵凭什么也能“求逆”特征分解要求方阵且最好对称现实里的数据矩阵往往是长条形的、甚至列之间线性相关。奇异值分解把限制全部去掉任意m×n矩阵都能写成UΣVᵀU 和 V 是正交阵Σ 是对角阵且对角元非负、按降序排列。奇异值平方就是AᵀA的特征值所以这套东西不是新工具而是特征分解在非方阵上的推广。伪逆A⁺ VΣ⁺Uᵀ的做法是把非零奇异值取倒数、零保持为零于是最小二乘解可以写成解析形式。import numpy as np rng np.random.default_rng(2) A rng.normal(size(6, 4)) U, s, Vt np.linalg.svd(A, full_matricesFalse) k 2 A_k U[:, :k] np.diag(s[:k]) Vt[:k, :] # 截断重构 print(s, np.linalg.norm(A - A_k) / np.linalg.norm(A)) # 截断相对误差 # 病态方程组直接求逆在数值上不可信 H np.array([[1.0, 1.0], [1.0, 1.0 1e-12]]) b np.array([2.0, 2.0]) print(np.linalg.cond(H)) # 极大 print(np.linalg.solve(H, b)) # 结果被舍入误差主导 print(np.linalg.lstsq(H, b, rcondNone)[0]) # 最小范数解稳定得多s是降序奇异值full_matricesFalse只保留前 min(m,n) 列省内存。rcond是截断阈值小于rcond * s.max()的奇异值被视为零这是处理病态矩阵的“后悔药”。很多人以为solve比lstsq高级其实在条件数上万时前者给出的解可能连符号都是错的。2.5 PCA 就是协方差矩阵的对角化把这套工具串起来看PCA 没有任何新数学先把数据按列去中心化算协方差矩阵对它做特征分解取最大的几个特征值对应的特征向量作为投影方向。之所以这么做是因为投影后各维方差等于对应特征值取最大特征值就是保留最多信息。马氏距离则是把欧氏距离放到白化后的空间里算d² (x-μ)ᵀΣ⁻¹(x-μ)Σ⁻¹ 正好是“按各方向方差做归一化”的算子因此它对特征之间的相关性和量纲差异免疫。KL 散度不对称这件事也常被误读D(p‖q)与D(q‖p)在 p 有值而 q 接近零时会爆炸常见的实现思路是用前者做“覆盖式”拟合、用后者做“择一式”拟合选哪一个取决于你更怕漏掉模式还是更怕生成不存在的模式。import numpy as np rng np.random.default_rng(3) X rng.normal(size(200, 5)) rng.normal(size(5, 5)) # 制造相关特征 Xc X - X.mean(axis0) # 必须先去中心 C Xc.T Xc / (len(Xc) - 1) # 无偏协方差shape(5,5) w, V np.linalg.eigh(C) idx np.argsort(w)[::-1] # eigh 升序翻转成降序 P V[:, idx[:2]] Z Xc P # 投影到二维 print(Z.shape, (w[idx] / w.sum())[:2]) # 解释方差比例3. 链式法则的账本反向传播为什么只跑一次前向就能算完梯度3.1 局部导数与计算图每个节点只关心自己的输入反向传播常被讲成“链式法则的应用”这个说法没错但太笼统容易让人以为要手推整条表达式。真正让它可工程化的思路是把前向计算拆成一串基本算子每个算子只负责算出自己输出对输入的局部导数反向时把上游传来的梯度乘上这个局部导数再往下发。前向过程中必须把激活值、输入这些中间量存下来因为多数局部导数依赖它们这就是“用显存换计算”的由来也解释了为什么 batch 调大后显存会先炸在激活缓存上而不是参数上。3.2 雅可比与向量-雅可比积反向模式便宜在哪严格讲一个算子对输入的导数是一个雅可比矩阵。前向模式每传播一次得到的是“一个输入方向变化引起的全部输出变化”JVP反向模式每传播一次得到的是“一个输出方向的梯度回传到全部输入”VJP。深度学习的损失是标量输出维度是 1所以反向模式一次就能拿到对所有参数的梯度如果反过来用前向模式要跑参数个数那么多次。这个取舍可以用一张表说清模式一次传播得到代价适合场景前向模式输入某一方向的全部输出导数与输入维度成正比输入少、输出多反向模式输出某一方向的全部输入梯度与输出维度成正比输出少、输入多标量损失所以“反向传播快”不是算法玄学而是维度结构带来的必然结果。理解了这一点再看框架里的backward()它做的就是沿着计算图逆序发 VJP一个不多一个不少。3.3 手推一个两层网络的梯度只看公式容易飘落到代码上才能发现哪里记错了账。下面这个例子用 tanh 激活、均方误差把前向、反向、数值校验写全。import numpy as np rng np.random.default_rng(0) X rng.normal(size(8, 4)) # 8 个样本4 维特征 y rng.normal(size(8, 1)) W1 rng.normal(scale0.5, size(4, 3)) b1 np.zeros(3) W2 rng.normal(scale0.5, size(3, 1)) b2 np.zeros(1) def forward(X, W1, b1, W2, b2): z1 X W1 b1 # (8,3) h np.tanh(z1) # 逐元素 z2 h W2 b2 # (8,1) return z1, h, z2 def mse(z2, y): return np.mean((z2 - y) ** 2) def backward(X, y, h, z2, W2): N X.shape[0] dz2 2 * (z2 - y) / N # dL/dz2(8,1)1/N 来自 mean dW2 h.T dz2 # (3,8)(8,1)(3,1) db2 dz2.sum(axis0) # (1,) dh dz2 W2.T # (8,1)(1,3)(8,3)VJP dz1 dh * (1 - h ** 2) # tanh(z)1-tanh(z)^2 dW1 X.T dz1 # (4,8)(8,3)(4,3) db1 dz1.sum(axis0) # (3,) return dW1, db1, dW2, db2 z1, h, z2 forward(X, W1, b1, W2, b2) dW1, db1, dW2, db2 backward(X, y, h, z2, W2) # 中心差分做梯度检查只抽查一个参数 eps 1e-6 W1[0, 0] eps lp mse(forward(X, W1, b1, W2, b2)[2], y) W1[0, 0] - 2 * eps lm mse(forward(X, W1, b1, W2, b2)[2], y) W1[0, 0] eps print(解析:, dW1[0, 0], 数值:, (lp - lm) / (2 * eps))几个容易出错的点都在注释里dz2除以 N 是因为损失用了mean而非sumdh dz2 W2.T是把输出端梯度映射回中间层形状必须是(8,3)1 - h**2用的是前向存下来的 h 而不是重算。校验时不要用绝对误差用|a-b| / max(1e-8, |a||b|)这类相对误差阈值取 1e-7 量级比较可靠因为中心差分本身的截断误差是 eps² 级、舍入误差是 1/eps 级eps 太大太小都不行。3.4 梯度下降步长、曲率与条件数有了梯度往反方向走多远就成了新问题。把损失在当前位置做二阶展开L(w - ηg) ≈ L(w) - η‖g‖² (η²/2)gᵀHg要让这一步不增大损失学习率大致不能超过2/λmaxλmax 是海森矩阵最大特征值。这解释了为什么同一个学习率在浅层网络上好好的换到深一点的网络就发散了曲率上去了。条件数λmax/λmin则决定收敛速度条件数越大等高线越扁梯度方向几乎垂直于最优方向走的是之字形。常见的应对思路是输入归一化、白化把协方差矩阵变成单位阵、用动量平滑方向本质都是在压条件数。4. 概率与统计模型的不确定性从哪来、怎么被量化4.1 条件概率、贝叶斯与先验更新联合概率表是最能把概率规则讲清楚的东西行是一个变量的取值列是另一个变量的取值格子里放联合概率。对行求和得到前者的边缘概率对列求和得到后者的边缘概率用联合除以边缘就得到条件概率。贝叶斯公式无非是把这条链式法则反过来用P(A|B) P(B|A)P(A)/P(B)其中P(A)是先验P(B|A)是似然分母是证据。把它和训练对应起来看参数的后验正比于先验乘似然每来一批数据就在旧后验上再乘一次似然所谓“在线更新”就是这个乘法过程。import numpy as np # 联合分布表行是 A 的取值列是 B 的取值 joint np.array([[0.12, 0.08, 0.10], [0.18, 0.32, 0.20]]) print(joint.sum()) # 必须为 1.0 pa joint.sum(axis1, keepdimsTrue) # 边缘化 A pb joint.sum(axis0, keepdimsTrue) # 边缘化 B cond_ba joint / pa # P(B|A) cond_ab joint / pb # P(A|B) print(cond_ba.sum(axis1)) # 每行和为 1 print(cond_ab.sum(axis0)) # 每列和为 1keepdimsTrue是为了保持二维形状否则广播会按行而不是按列对齐算出来的根本不是条件概率。这类“看着对、其实错”的代码是数据统计里最常见的翻车方式检查办法就是看归一化后的和是不是全 1。4.2 中心极限定理与大数定律小批量凭什么能代表整体大数定律说的是样本均值依概率收敛到期望样本量越大越稳中心极限定理进一步给出波动幅度均值近似服从正态分布标准差是σ/√n。这意味着 batch size 从 32 提到 128梯度的标准差只降到原来的一半而不是四分之一收益递减得非常快这也是大批量训练需要配合学习率缩放的经验来源之一。反过来当数据存在长尾或极端值时均值的收敛速度会被大方差拖慢此时用中位数、分位数描述数据比均值更稳箱线图能一眼看出偏态和离群点。4.3 相关与假设检验系数不是因果检验有前提Pearson 相关系数度量的是线性关系Spearman 则是把数据转成秩之后算 Pearson度量的是单调关系。数据是强单调但非线性时Pearson 会明显低估关联强度甚至因为个别离群点从 0.9 掉到 0.3Spearman 对离群点不敏感代价是丢掉“变化多快”的信息。t 检验假设样本近似正态、方差可比样本小又分布偏时结论不可靠Mann-Whitney U 检验只比较秩不假设分布形态适合小样本和非正态数据。做 A/B 对比时先看分布形状再选检验方法比事后解释 p 值靠谱得多。import numpy as np from scipy import stats x np.linspace(0, 10, 50) y x ** 3 # 单调但强非线性 print(stats.pearsonr(x, y)) # 高但没刻画曲率 print(stats.spearmanr(x, y)) # 1.0单调关系完全捕获 a np.random.default_rng(0).normal(0, 1, 30) b np.random.default_rng(1).normal(0.5, 3, 30) print(stats.ttest_ind(a, b, equal_varFalse)) # 方差不齐用 Welch print(stats.mannwhitneyu(a, b, alternativetwo-sided))5. 避坑与排查五个能把训练结果带偏的数学误用5.1 数值稳定性NaN 从哪冒出来现象训练几十步后 loss 突然变成nan或者前几轮正常、学习率调大后立刻崩。原因通常有三类对数里出现 0、分母接近 0、指数溢出。softmax 直接用exp再除以和输入一大就溢出成inf/inf交叉熵里log(0)直接给出负无穷。解决思路是把 softmax 与 log 合并成 logsumexp 形式先减去该行最大值再取指数数学上等价、数值上安全概率做除法前加一个极小量并做 clip同时监控每层激活的最大绝对值超过阈值就说明前向已经不稳。5.2 形状与索引静默广播最伤人现象代码不报错、loss 也在降但精度明显低于预期。原因往往是一个(N,)和一个(N,1)相减广播成(N,N)的矩阵损失算的是两两之差而不是逐样本之差梯度方向全歪。另一种情况是转置或切片返回的是视图而不是拷贝后续原地修改悄悄改掉了原始数据。解决办法是养成两个习惯每个张量创建时在注释里写明各维语义关键运算后打印一次 shape对可能共享内存的操作显式.copy()并且在训练循环里断言 batch 维等于配置值。5.3 概率表没归一化条件概率算成了联合概率现象写了个“条件概率”矩阵行和不是 1用它算出来的期望和实际样本均值对不上。原因是直接拿联合概率表当条件概率用了忘了除以边缘概率。解决办法固定成三步先验证联合表总和为 1再按需要沿对应轴求和并keepdimsTrue最后做除法并断言结果为 1。这套检查可以写成几行断言塞进数据预处理脚本跑一次就够。5.4 拿 inv 解方程条件数一大就不可信现象线性回归的闭式解和梯度下降的结果对不上甚至系数符号相反。原因是协方差矩阵接近奇异直接求逆把舍入误差放大成了主项。解决办法是用lstsq或 SVD 求伪逆设置合理的截断阈值同时打印条件数条件数超过 1e6 时与其调求解器不如先做特征标准化或加一点正则项把问题本身改好。5.5 相关系数当因果、当特征重要性现象两个特征相关系数 0.95删掉其中一个后模型效果反而变差。原因是相关系数只描述两两线性关系不反映在给定其他特征后的边际贡献也不反映非线性关系。解决办法是先画散点图看形状非线性就用秩相关或互信息做特征筛选时优先看模型侧的重要性或做消融实验别把相关系数当结论。6. 手推还是自动微分布局约定才是真正的分歧点真正让人怀疑自己数学功底的不是推导能力而是约定。矩阵微积分里同一个导数分子布局和分母布局给出的形状互为转置书上用一种、框架里用另一种于是“公式抄对了但形状不对”成了最常见的返工原因。附件里专门用一章列了各种情形的公式与恒等式价值就在于把这些约定固定下来标量对向量、向量对标量、向量对向量、标量对矩阵各自的雅可比排布方式写清楚后面推反向传播时就不会来回反复。方式优点代价适用手工推导每一步可控能发现结构性问题易错、难维护小规模验证、写自定义算子数值差分无需推导天然可信O(n) 次前向精度有限梯度检查自动微分一次反向拿全部梯度计算图与内存开销生产训练我一般会这么理解三者的关系自动微分是主力数值差分是检查工具手工推导是理解工具三者不替代。实践中真正管用的流程是三步先在纸上写出每个张量的阶数和各维含义再用中心差分在随机初始化的极小网络上抽查几个参数最后才把它放进完整训练。跳过第一步后面所有报错都会变成猜谜跳过第二步梯度写错也能训练出“看起来还行”的模型等换数据才暴露。还有个取舍值得说清PCA 这类基于特征分解的方法给出了解析解可解释、可复现但只对线性结构有效用神经网络学表征灵活却把可解释性换成了黑匣子。选哪个不取决于哪个“更先进”而取决于你能不能接受在出问题时无法定位原因。同理条件数高的时候是该做白化还是该换优化器本质是“改问题”和“改算法”的取舍前者更根本后者更快见效。血泪经验是我曾经花了一整晚排查一个梯度不收敛的问题最后发现是损失函数用了sum而反向传播里按mean除了样本数只差一个常数因子学习率刚好卡在发散边界上。从那以后我每次写自定义损失都强制先跑一遍相对误差小于 1e-6 的梯度检查再开训练。数学不会替你决定架构但它能在你怀疑框架的时候给你一个可以验证的答案。这份资料里从数组基础到反向传播的那条线正好可以照着走一遍配合代码逐节核对比反复读公式有用得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑