资讯详情

深度学习基础与工程实践:公式代码对照及Transformer避坑

📅 2026/10/30 8:40:11 | 华诺云谱 👁 阅读
深度学习基础与工程实践:公式代码对照及Transformer避坑
简介《Deep Learning: Foundations and Concepts》是Christopher M. Bishop与Hugh Bishop合著、Springer于2024年出版的深度学习教材电子版面向机器学习研究生、算法工程师和科研人员帮助读者建立从神经网络基础到现代深度模型的系统认知。全书强调概念与原理覆盖多层网络训练、Transformer架构、自然语言处理、图神经网络等主题并讨论大语言模型等前沿方向。资源包内含1个PDF文件大小约46.96MB便于在电脑或平板上检索阅读。作者Christopher Bishop曾撰写经典教材PRML并任微软剑桥研究实验室主任、微软技术院士及爱丁堡大学教授内容兼具理论深度与教学可读性。目前已有702人学习下载适合作为课程参考、科研查阅和进阶自学的系统读物。1. 模型训练不收敛时「回去翻公式」这条路的实战读本调一个 Transformer 训练不收敛loss 在前 200 步像心电图一样抖动你翻遍训练脚本学习率、warmup、梯度裁剪都调过了最后还是靠某篇博客里一句“LayerNorm 放在了残差分支外侧”救回来。这类问题的根子往往不在工程细节而在对深度学习基础概念的理解是否扎实。这本 2024 年出版、由两位作者合写的《Deep Learning: Foundations and Concepts》是少见的既讲概率与线性代数底层推导、又把 Transformer 架构、图神经网络、生成模型串成一条线的读本作者二十多年前写过一本从概率视角讲模式识别的经典教材这本可以看作那条路线的续篇。它适合两类人刚从 NLP 转进来、公式看得懂但推不动的人和工程做得熟、想知道每行nn.MultiheadAttention背后到底在算什么的人。2. 拆目录二十多个小章怎么排成一条不回头的时间线书的结构很反常不是六个大块而是拆成二十多个更小的章每章只依赖前面出现过的东西。作者在序里直说了这种线性结构是为两个学期的课准备的也适合自学和研究的人随时插进去读。这条设计直接决定读法——它不适合当参考手册跳着翻一旦跳章后面某个符号你会不知道从哪来。2.1 前置数学的三块地基与两个附录书里把机器学习需要的数学压到三块概率论、线性代数、多元微积分。概率论是自包含地讲从基本分布一路推到后面变分推断要用的东西线性代数放在附录里做结果汇总多元微积分则假定读者已经会另外补了变分法和拉格朗日乘子两个附录这两个是后面求 ELBO、做约束优化的工具。这个配置意味着一个很现实的门槛判断如果你能对多元函数求偏导、会写雅可比矩阵这本书从第 3 章开始你就能顺畅读如果不会先用两周补链式法则和矩阵求导别硬啃否则读到反向传播那章会卡在“为什么这里要转置”上出不来。还有个容易忽略的定位信息作者明确说这本书几乎完全是非贝叶斯的。上一本经典教材里花了大量篇幅的贝叶斯方法在这本里基本被砍掉了只留下做生成模型必须的那部分。所以如果你的诉求是贝叶斯神经网络、变分推断的完整贝叶斯框架这本书给不了你它给的是一条从频率派视角把深度网络讲透的路线。2.2 习题三档难度与算法框的正确用法每章末尾都有习题并且按难度标了三档一个问号表示几分钟能做完两个问号是中等三个问号是需要认真推的复杂题。书里还有一类独立的算法框把正文讨论的关键算法单独汇总成伪代码。这两样东西的正确用法直接决定你读完是“好像懂了”还是“真能用”。我的建议是一档题全部做用来确认概念没读错二档题挑和你工作直接相关的做三档题至少读一遍题目和答案知道边界在哪。算法框不要当实现抄作者自己就说明了这些框不处理计算效率问题只作为数学解释的补充——意味着你照着框写成 numpy 能跑但拿到真实数据规模上会直接 OOM。配套材料里有三样值得用起来数字版、全部习题的解答、以及图和插画的 PDF/JPEG 源文件。图源这条尤其有用做内部分享或者写技术文档时能直接引用原图比截图清晰得多。2.3 三条阅读路线按你的背景选别通读下面这张表是我按书的线性依赖关系整理出来的三条路线对应三类人路线之间不通用读者类型起点必读章可跳过预计投入刚转进来公式能看不能推概率论基础章概率 → 单层网络 → 多层网络 → 梯度下降与反向传播 → 卷积 → Transformer采样与蒙特卡洛第一遍68 周工程熟手想补底层单层网络反向传播 → 正则化 → Transformer → 图神经网络概率论前半会查就行34 周做生成模型复现概率论基础章概率 → 变分推断 → 离散/连续隐变量 → 生成模型 → 扩散模型卷积网络若已熟45 周路线二最容易翻车。很多工程背景的人觉得“反向传播我天天用”直接从 Transformer 章切入结果读到注意力里那个缩放因子为什么是根号 d 时卡住因为那背后是一个方差分析的结论在前面的概率章里。我的建议是哪怕走路线二也先把概率章里关于期望、方差、高斯分布的三节读完一共不到两小时能省掉后面反复回翻的时间。路线三要注意顺序变分推断那部分必须在连续隐变量之前读否则 ELBO 的推导你会以为是天上掉下来的。这块后面第 4 章我会给一个最小可跑的闭环。3. 记号约定落地把 x、X、E[·]、cov[·] 逐个写成可运行的对照这本书最值得抄下来贴墙上的其实是开头那几页数学记号约定。它把向量、矩阵、转置、单位矩阵、全 1 向量、拼接、行列式、采样记号、期望、方差、协方差、邻域、泛函、渐进上界、下取整、独立同分布、数据矩阵的形状全部定死了。定了之后全书不改这是它能保持推导连贯的原因也是你写代码时最容易和它拧着来的地方。3.1 一张符号到代码的对照表先看约定向量用小写粗体矩阵用大写粗体向量默认是列向量上标 T 表示转置所以 x^T 是行向量单位矩阵记 I维数不混淆时省略下标1 表示元素全为 1 的列向量⊕ 表示向量拼接|x| 是绝对值det A 是行列式x ∼ p(x) 表示从分布采样E_x[f(x,y)] 是期望无歧义时省略下标var[f(x)] 是方差cov[x,y] 是协方差cov[x] 是 cov[x,x] 的简写∀ 表示对所有ℝ 是实数集图上节点 i 的邻居集合记 N(i)书里特意提醒别和高斯分布 N 混淆泛函记 f[y]O 表示渐进上界⌊x⌋ 是下取整。最关键的是一条N 个 D 维样本组成的观测写成 N×D 的数据矩阵 X第 n 行是行向量 x_n^T元素记 x_ni表示第 n 个样本的第 i 个分量。一维变量的样本矩阵则用小写 x 表示一个列向量。也就是说书里两种字体区分了“N 维列向量”和“D 维向量”这在排版上是清楚的在代码里完全看不出来。书中记号含义numpy / torch 对应易错点xD 维列向量x.shape (D,)或(D,1)别默认成行向量x^T行向量x.T/x[None, :]一维数组.T不改变形状XN×D 数据矩阵X.shape (N, D)转置与否直接影响协方差方向I单位矩阵np.eye(D)广播时别写成标量 11全 1 列向量np.ones((D,1))与标量 1 完全不同a ⊕ b向量拼接np.concatenate([a,b])拼接维度要显式写 axisx ∼ p(x)采样torch.distributions采样后记得 detachE[·]期望蒙特卡洛均值x.mean()沿哪个轴求均值cov[x]协方差矩阵np.cov(X, rowvarFalse)rowvar默认是 TrueN(i)节点 i 的邻居集合邻接矩阵第 i 行非零列别和高斯分布搞混O(g)渐进上界复杂度分析不是精确计数这张表建议直接抄进你的笔记每次写代码前扫一眼能省掉大量“形状对不上”的调试时间。3.2 从公式到张量以协方差和期望为例书里定义 cov[x] 是 cov[x,x] 的简写这个简写在推导里省事落到代码里就要显式处理“沿哪一维求”的问题。下面这段是把 X 是 N×D 这个约定真正用起来的写法import numpy as np rng np.random.default_rng(0) N, D 1000, 4 # 严格按书里约定X 是 N×D第 n 行是第 n 个样本的转置 X rng.normal(size(N, D)) * np.array([1.0, 2.0, 0.5, 3.0]) # E[x]沿样本维axis0求均值结果是一个 D 维向量 mu X.mean(axis0) # shape (4,) # 中心化广播方向自动对齐到 (N, D) - (D,) Xc X - mu # cov[x]先转置成 D×N 再乘得到 D×D除以 N-1 是无偏估计 cov Xc.T Xc / (N - 1) # shape (4, 4) # 与库函数对照rowvarFalse 才表示每列是一个变量 cov_lib np.cov(X, rowvarFalse) print(mu.shape, cov.shape, np.allclose(cov, cov_lib, atol1e-10))这段代码有三个参数值得说清。axis0是整段的关键书里 X 的第 n 行是一个样本所以“对样本求平均”就是沿第 0 维压缩得到 D 维的均值向量如果你按习惯写成axis1得到的是 N 维的“每个样本自己的均值”形状对不上后面的协方差计算。Xc.T Xc的顺序也不能反Xc 是 N×D转置后是 D×N乘起来才是 D×D写成Xc Xc.T会得到 N×N那是样本间的 Gram 矩阵不是协方差。N-1是无偏估计的除数样本量小时这个差别肉眼可见N 上千后基本可以忽略但概念上要清楚。np.cov的rowvar参数默认是 True意思是“每一行是一个变量”和书里的约定正好相反。这个默认值坑过太多人我一般会强制写rowvarFalse哪怕看起来啰嗦。3.3 数值稳定性softmax、log-sum-exp、slogdet 三处必改书里给的公式是数学形式直接翻译成代码会在真实数据上出问题。有三个地方几乎必改。第一个是 softmax。书里定义成 exp 归一化直接写np.exp(x) / np.exp(x).sum()在 x 有 800 以上的分量时溢出成 inf再除就得到 nanimport numpy as np def softmax(x, axis-1): # 减去每行最大值数学上等价数值上把 exp 的参数压到 0 x_shift x - x.max(axisaxis, keepdimsTrue) e np.exp(x_shift) return e / e.sum(axisaxis, keepdimsTrue) x np.array([1000.0, 1001.0, 1002.0]) print(softmax(x)) # [0.0900 0.2447 0.6652] print(np.exp(x) / np.exp(x).sum()) # nankeepdimsTrue不能省否则(3,)减()靠广播能过但换成(B, T)的批量输入时行最大值形状是(B,)减(B,T)就会广播错方向静默给出错误结果——这是最难受的一类 bug不报错但结果全错。第二个是 log-sum-exp。凡是要算 log(Σ exp(x)) 的地方包括交叉熵、log 似然、变分下界都要用同一个技巧提出最大值再算。第三个是行列式np.linalg.det在矩阵维度稍大时下溢成 0改成np.linalg.slogdet拿符号和对数行列式再 exp 回去。这三个改动都不影响数学结论但决定了你的代码在 float32 下能不能跑到收敛。4. 主线打穿attention、消息传递和 ELBO 的三十行实现书的后半段把现代架构拆成几条主线Transformer 与注意力、图神经网络、生成模型与扩散。这些章节的共同点是把每个模块还原成“几个矩阵乘 一个非线性”公式给完还有算法框。照着框写一遍比读十遍公式有用。4.1 Transformer 与注意力三项公式与形状约定注意力在书里被拆成三步打分、归一化、加权求和。打分用查询和键的内积再除以根号 d_k这个缩放因子不是玄学是为了让内积的方差保持在不随维度增长的水平否则维度一大 softmax 就退化成 one-hot梯度几乎为零。import numpy as np def softmax(x, axis-1): x x - x.max(axisaxis, keepdimsTrue) e np.exp(x) return e / e.sum(axisaxis, keepdimsTrue) def scaled_dot_attention(Q, K, V, maskNone): Q: (Tq, d_k) 查询 K: (Tk, d_k) 键 V: (Tk, dv) 值 mask: (Tq, Tk) 的 bool 数组True 表示可见 返回: (Tq, dv) 的输出以及 (Tq, Tk) 的注意力权重 d_k Q.shape[-1] scores Q K.T / np.sqrt(d_k) # (Tq, Tk) if mask is not None: scores np.where(mask, scores, -1e9) # 屏蔽位压到极小softmax 后趋近 0 A softmax(scores, axis-1) return A V, A rng np.random.default_rng(1) Tq, Tk, d_k, dv 4, 6, 8, 5 out, attn scaled_dot_attention(rng.normal(size(Tq, d_k)), rng.normal(size(Tk, d_k)), rng.normal(size(Tk, dv))) print(out.shape, attn.shape, attn.sum(axis-1))形状约定是这段代码里最容易错的地方值得单独记Q 的行数是查询的个数 TqK 和 V 的行数必须相同都是键值对的个数 TkK 的列数必须等于 Q 的列数 d_k因为要做内积V 的列数 dv 可以任意它决定输出宽度。输出形状是 (Tq, dv)注意力矩阵是 (Tq, Tk)每行求和为 1。-1e9这个替换值不能写成 0因为 0 经过 softmax 后不是 0 而是有实际权重也不能写成-np.inf虽然数学上对但在某些混合精度路径下会产生 nan。用足够大的负数是最稳的做法这也是书里算法框之外、需要工程经验补的那部分。多头注意力的做法是把 d_model 切成 h 份每份独立做上面这套最后拼回来再乘一个输出投影。切分用 reshape 加 transpose代码里要特别注意 head 维和序列维的顺序(B, T, h, d_head)转成(B, h, T, d_head)之后才能做批量矩阵乘。4.2 图神经网络把邻居聚合写成一次稀疏矩阵乘图神经网络那章的核心是消息传递每个节点收集邻居的特征聚合再和自己原来的特征合并过一层线性变换和非线性。书里用 N(i) 表示节点 i 的邻居集合写成求和形式很直观直接照着写就是双重循环import numpy as np def gnn_naive(H, A, W_self, W_neigh): # H: (N, F_in)A: (N, N) 邻接矩阵A[i][j]1 表示 j 是 i 的邻居 N H.shape[0] agg np.zeros_like(H) # (N, F_in) for i in range(N): for j in range(N): if A[i, j]: agg[i] H[j] # 累加邻居特征 return H W_self agg W_neigh # 自环项 邻居项 def gnn_layer(H, A, W_self, W_neigh): # 同样的语义用一次矩阵乘把双重循环消掉 deg A.sum(axis1, keepdimsTrue) deg[deg 0] 1.0 # 孤立点防止除零 A_norm A / deg # 行归一化抑制高度数节点 return H W_self A_norm H W_neigh两段代码语义相同差别全在复杂度双重循环是 O(N²F)N 上万时 Python 层跑不动矩阵乘版本把聚合变成一次 (N,N)×(N,F) 的乘法交给 BLAS同样的 N 能快三个数量级。这是书里算法框没有覆盖、但工程上必须自己补的一步。行归一化那两行值得展开。不归一化时一个度数为 5000 的节点聚合出来的特征幅值是度数 5 的节点的 1000 倍前向传播几层就爆。除以度数是最简单的处理更常用的是对称归一化 D^(-1/2) A D^(-1/2)效果更稳实现上就是先算d_inv_sqrt 1/np.sqrt(deg)再两侧各乘一次。deg 0的保护不能省孤立点在真实图里很常见不处理会出现 nan 并污染整批。4.3 生成模型从 ELBO 到重参数化的最小闭环生成模型那条线从变分推断讲到连续隐变量再到扩散。变分自编码器的损失在书里是两项重构项和 KL 项。KL 项对高斯先验有解析解不需要采样估计这在实际训练里能省掉大量方差。import torch import torch.nn.functional as F def vae_loss(x, x_rec, mu, logvar): x, x_rec: (B, D) 原始输入与重构 mu, logvar: (B, K) 编码器输出的均值与对数方差 # 重构项高斯解码器且方差固定时负对数似然正比于平方误差 rec F.mse_loss(x_rec, x, reductionnone).sum(dim1) # (B,) # KL(q(z|x) || N(0,I)) 的解析解逐样本计算 kl -0.5 * torch.sum(1 logvar - mu.pow(2) - logvar.exp(), dim1) # (B,) return (rec kl).mean() B, D, K 32, 20, 8 x torch.randn(B, D) mu, logvar torch.randn(B, K), torch.zeros(B, K) x_rec x 0.1 * torch.randn(B, D) loss vae_loss(x, x_rec, mu, logvar) loss.backward() if loss.requires_grad else None print(float(loss))这里有两个参数选择必须说清。编码器输出的是logvar而不是标准差原因是标准差必须为正直接回归容易出负值用对数方差再exp就能保证非负而且数值范围更宽松。KL 项里那个-0.5 * (1 logvar - mu² - exp(logvar))是逐维求和再逐样本累加dim1不能写错写成dim0就变成跨 batch 求和损失尺度会随 batch size 变化学习率得跟着调非常麻烦。重构项用 MSE 等价于假设解码器是固定方差的高斯如果你想让模型自己学方差就得换成完整的高斯负对数似然多一项 log σ²。这个区别在书里讲得比较细实际用的时候大多数人直接取 MSE因为效果差别不大但省一个输出头。重参数化那步在这段代码里没体现它是前向的一部分z mu torch.exp(0.5 * logvar) * epseps从标准正态采样。这行必须可导所以不能直接用torch.normal(mu, std)那个采样操作会切断梯度。这是新手最常见的翻车点之一模型能跑但编码器永远学不动。5. 避坑记录读这本书最容易翻车的五处5.1 形状对不上x 和 X 是两种东西现象照着公式写协方差、写注意力跑起来报shapes not aligned或者更糟——不报错但结果明显不对。原因书里用不同字体区分了 N 维列向量和 D 维向量还明确了 X 是 N×D、第 n 行是 x_n^T。这个约定在纸上很清晰在代码里一维数组的.T根本不改变形状np.cov的rowvar又默认是 True两处叠加就是灾难。解决所有涉及样本矩阵的地方统一用 N×D函数入口处加一行assert X.ndim 2协方差一律显式写rowvarFalse。我自己在项目里会把“样本在行、特征在列”写进模块 docstring 第一行比记在心里可靠。5.2 跳过概率章直接读 Transformer现象读到注意力缩放因子为什么是根号 d_k 时卡住读到扩散模型的变分下界时完全跟不上只能靠背结论。原因书的线性依赖是真的线性每个结论只依赖前面出现过的内容。缩放因子背后是内积方差随维度线性增长的结论那个结论在概率章ELBO 的推导依赖 KL 散度和期望的线性性也在前面。解决哪怕工程背景很强也先把概率章里期望、方差、高斯分布、KL 散度四节读完两小时以内。这四节是整个后半本书的地基回补的成本远低于硬啃。5.3 把算法框当生产代码现象照着书里的算法框写完小数据跑得挺好换到真实数据规模直接内存爆掉或者跑一整夜。原因作者明确说明了算法框不处理计算效率问题只作为数学解释的补充。图神经网络的邻居聚合框是双重循环注意力框是单头无批量的版本都是为可读性写的。解决算法框只用来确认“我理解了这一步在算什么”实现时换成矩阵乘或稀疏矩阵乘。判断标准很简单只要代码里出现了对 N 或 T 的二重循环就说明还没写完。5.4 只读不做题现象每个公式都觉得看懂了合上书自己推一遍第三步就推不下去。原因书里的推导为了紧凑中间步骤跳得比较多尤其是变分推断和反向传播那几章。习题的设计目的就是补这些跳跃三档难度正好对应“验证理解”“扩展结论”“完整推导”。解决一档题全做二档题挑相关章节做三档题至少看题目和答案。这套题配了完整解答卡住二十分钟就去看答案别耗——目的是理解不是证明自己能推。5.5 抱着“追最新研究”的预期来读现象翻完发现没有最新的大模型论文解读也没有某个刚火的架构细节觉得这本书过时。原因作者在序里就写明了刻意不做最新研究的全面综述价值在于提炼那些经得起时间检验的基础。书里举的例子是大语言模型这几年迭代极快但底层的 Transformer 和注意力机制五年没大变很多核心原理几十年前就有了。解决把这本书定位成“地基 查表”追前沿另找渠道。它真正省时间的地方是当你遇到一个说不清的现象时能在这里找到对应章节从公式层面判断是数据问题、优化问题还是架构问题。6. 进阶用法建一份公式-代码双索引用数值梯度验收读到第三遍的时候我发现这本书最有效的用法不是通读而是建一份双向索引左边是公式和它的章节位置右边是我自己写的、跑通过的最小实现。这份索引不用好看用文本文件就行每一行长这样KL(q||p) 解析解 | 变分推断章 | vae_loss() 里的 kl 那行 | 验证mu0,logvar0 时值为 0 缩放点积注意力 | Transformer 章 | scaled_dot_attention() | 验证mask 全 False 时输出全 0 协方差 cov[x] | 概率章 | 3.2 节代码 | 验证与 np.cov(rowvarFalse) 对齐这份索引的价值在于下次再遇到类似前文那种“模型不收敛”的问题时你不是从零开始猜而是从索引里挑出可能相关的三五行代码跑一遍验证。写索引的过程本身也是验收——你要是写不出“怎么验证这一行”说明这章还没真读懂。验证手段里我最常用的是数值梯度因为它不依赖任何框架的自动微分纯靠定义就能判断一个实现对不对import numpy as np def numerical_grad(f, x, eps1e-6): 中心差分对每个分量单独扰动返回数值梯度 grad np.zeros_like(x, dtypefloat) it np.nditer(x, flags[multi_index]) for _ in it: idx it.multi_index orig x[idx] x[idx] orig eps fp f(x) x[idx] orig - eps fm f(x) x[idx] orig grad[idx] (fp - fm) / (2 * eps) return grad # 用数值梯度校验手写的 log-sum-exp 梯度 def lse(x): m x.max() return m np.log(np.exp(x - m).sum()) def lse_grad_manual(x): e np.exp(x - x.max()) return e / e.sum() x np.array([0.3, -1.2, 2.4]) print(np.allclose(numerical_grad(lse, x.copy()), lse_grad_manual(x), atol1e-6))eps取 1e-6 是折中太大截断误差明显太小浮点抵消会让结果失真。float64下这个值基本通吃float32下要放大到 1e-3 左右否则差分被舍入误差吃掉。这段代码对任何标量输出函数都适用我用它校验过 softmax 的雅可比、ELBO 对均值的梯度、以及自己写的图卷积聚合比读十遍推导都快。一个具体技巧书里每出现一个新的损失函数先在纸上对它求一次导再用数值梯度对一遍两边对上了再写进正式代码。这个习惯听起来笨但它把“公式理解错”和“代码写错”这两类问题分开了——数值梯度对不上是公式推错对上了但训练不收敛是代码或超参问题。省下来的排查时间远超这点投入。从那以后我每次把书里的公式搬进代码都强制先跑一遍数值梯度验收哪怕是已经用过很多次的损失函数。公式-代码双索引这份文件现在攒了两百多行成了我处理训练问题时的第一站。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑