简介:本资源为2022年出版的深度学习数学基础权威指南《Math for Deep Learning》,面向Python实践者、转行AI的学习者及数学基础薄弱但希望深入理解神经网络原理的工程师。全书系统覆盖线性代数(向量/矩阵运算与张量表示)、概率统计(数据建模与不确定性量化)、微积分(梯度推导与反向传播本质)及优化算法(SGD、Adam等收敛性分析),并结合NumPy、PyTorch等库提供可运行的数学实现示例,助力读者将抽象公式转化为代码逻辑。资源为单文件PDF,共1个7.71MB高清电子书,排版规范、公式清晰,含完整目录、章节习题与技术审校注释,便于逐章精读与公式速查。目前已有84人下载学习,适合作为深度学习理论补强的核心参考书,尤其适合在复现论文模型或调试梯度异常时回溯数学根源。
1. 这本书不是“数学考试复习资料”,而是你调试神经网络时突然卡住、翻遍代码却找不到问题根源时,能让你拍着大腿说“哦!原来是这里”的那本手册
你有没有过这样的时刻:模型训练 loss 突然炸开,梯度变成 nan;明明改了学习率,结果收敛更慢;用 PyTorch 写完反向传播,自己都看不懂grad_fn=<AddBackward0>到底在算什么;甚至看别人调参像呼吸一样自然,而你连为什么 BatchNorm 要用均值和方差做归一化都讲不全——不是你不努力,是手里的工具书没对上你的“痛感”。
《Math for Deep Learning》这本书,就是专为这种“卡壳现场”写的。它不考你洛必达法则的七种变形,也不逼你手推 20 行行列式;它只问一个问题:“当你在 Python 里调用torch.nn.Linear(784, 128),背后到底发生了什么数学动作?”——然后用你能看懂的符号、你能复现的 NumPy 代码、你能画出来的图,把线性变换、概率建模、链式求导、矩阵微分一层层剥开给你看。它适合所有正在用 Python 写模型、但每次遇到报错信息就下意识搜 Stack Overflow 的人:可能是刚学完《Python Crash Course》想进深度学习坑的转行者,也可能是已经能跑通 ResNet 却总在调参时靠玄学碰运气的工程师。它不假设你记得大一高数期末考了啥,但默认你愿意在 Jupyter 里敲几行np.dot()验证一个想法。
2. 先搞懂一个事实:你写的每一行 PyTorch 代码,都在悄悄执行一套数学协议
深度学习框架再智能,也不会替你回答这三个问题:
- 为什么
x @ W.T + b是全连接层的标准写法,而不是W @ x + b? - 为什么
nn.CrossEntropyLoss()内部要先算 softmax 再算 log,而不是直接用 one-hot 标签和 logits 做 MSE? - 为什么
torch.autograd能自动求导,但你手动实现梯度更新时,偏偏在W -= lr * grad_W这一步总出错?
这些问题的答案,不在 PyTorch 文档的 API 列表里,而在你跳过的数学定义中。这本书的前四章(概率、统计、线性代数、微积分)不是让你“系统学习”,而是给你配一套「神经网络解剖刀」——刀刃是概念,刀柄是 Python 实现。我们不从公理出发,而从你明天就要跑的代码出发。
2.1 你真正需要的线性代数,不是行列式,而是“形状对齐”
在 PyTorch 或 NumPy 中,张量运算失败,90% 是因为 shape 不匹配。比如:
import numpy as np # 假设这是你的输入:batch=32, feature=784(一张 28x28 图片展平) x = np.random.randn(32, 784) # shape: (32, 784) # 假设这是你的权重:784 维输入 → 128 维输出 W = np.random.randn(784, 128) # shape: (784, 128) # ✅ 正确:矩阵乘法要求 (m,n) @ (n,p) → (m,p) y = x @ W # shape: (32, 128) print("输出形状正确:", y.shape) # (32, 128) # ❌ 错误:W.T 是 (128, 784),x @ W.T → (32, 784) @ (128, 784) → 形状不兼容! try: y_wrong = x @ W.T except ValueError as e: print("报错原因:", e) # operands could not be broadcast together with shapes (32,784) (128,784)提示:
@是 NumPy/PyTorch 的矩阵乘法符号,等价于np.matmul()。它严格检查维度:左边张量的最后一个轴,必须等于右边张量的倒数第二个轴。这不是语法限制,而是线性变换的数学本质——输入向量空间维数,必须匹配权重矩阵的列数。
这个例子背后,是书中第 5 章反复强调的“向量空间映射”思想:W不是一堆数字,而是一个从 ℝ⁷⁸⁴ 到 ℝ¹²⁸ 的线性映射函数。x @ W就是在调用这个函数。你不需要背诵“线性映射满足加法和数乘保持性”,但你要养成习惯:每次写@之前,先默念“我的输入维度是多少?权重矩阵的哪一维承接它?输出会落到哪个空间?”
2.2 概率不是“抛硬币多少次”,而是你模型输出的“可信度说明书”
你在写分类任务时,是不是经常这样:
logits = model(x) # shape: (32, 10),10 个类别的原始分数 probs = torch.softmax(logits, dim=1) # shape: (32, 10),每行和为 1 pred = probs.argmax(dim=1) # shape: (32,)但有没有想过:为什么不用sigmoid?为什么softmax要按行归一化?为什么CrossEntropyLoss要把logits和labels一起喂进去,而不是先softmax再nll_loss?
答案藏在第 2、3 章的“概率分布建模”里。简单说:深度学习的分类目标,是让模型输出一个概率分布(比如“这张图是猫的概率 0.82,狗 0.15,鸟 0.03”),而真实标签是一个one-hot 分布(比如“猫=1,狗=0,鸟=0”)。衡量两个分布的差异,最自然的工具是KL 散度(Kullback-Leibler Divergence)——而CrossEntropyLoss正是 KL 散度的简化计算形式(当真实分布是 one-hot 时,KL 散度 = 交叉熵 = -log(p_true_class))。
你可以用 NumPy 快速验证:
def cross_entropy_numpy(logits, labels): """logits: (N, C), labels: (N,) of class indices""" N = logits.shape[0] # 手动 softmax(避免数值溢出,减去每行最大值) logits_shifted = logits - logits.max(axis=1, keepdims=True) exp_logits = np.exp(logits_shifted) probs = exp_logits / exp_logits.sum(axis=1, keepdims=True) # 取每个样本对应真实类别的概率,取负对数 true_probs = probs[np.arange(N), labels] return -np.log(true_probs + 1e-15).mean() # 加小常数防 log(0) # 模拟一个 batch logits = np.array([[2.1, 0.5, -1.2], # 样本0:猫得分最高 [0.3, 1.8, 0.9]]) # 样本1:狗得分最高 labels = np.array([0, 1]) loss = cross_entropy_numpy(logits, labels) print(f"手动计算交叉熵: {loss:.4f}") # ~0.3628这段代码没有调用任何深度学习框架,但它复现了 PyTorchCrossEntropyLoss的核心逻辑。你看懂了它,下次看到 loss 曲线震荡,就不会只调 learning rate,而会先检查:我的 logits 输出范围是否合理?softmax 后的概率分布是否过于尖锐或平坦?——这就是数学给你的“调试直觉”。
2.3 微积分不是求导公式表,而是你理解backward()的操作手册
loss.backward()这行代码,是深度学习里最像黑匣子的操作。但它的原理,就是第 7、8 章讲的链式法则和矩阵微积分。我们用一个极简例子拆解:
import numpy as np # 假设这是一个单层网络:y = sigmoid(W @ x + b) x = np.array([1.0, 2.0]) # 输入 W = np.array([[0.5, -0.3]]) # 权重 (1,2) b = np.array([0.1]) # 偏置 (1,) y_true = np.array([0.8]) # 真实输出 # 前向传播 z = W @ x + b # z = 0.5*1 + (-0.3)*2 + 0.1 = 0.0 y_pred = 1 / (1 + np.exp(-z)) # sigmoid(z) = 0.5 # 定义损失:MSE loss = 0.5 * (y_pred - y_true)**2 # 现在,手动求 loss 对 W 的梯度(即 ∂loss/∂W) # 链式法则:∂loss/∂W = ∂loss/∂y_pred * ∂y_pred/∂z * ∂z/∂W dloss_dy = y_pred - y_true # ∂loss/∂y_pred = (y_pred - y_true) dy_dz = y_pred * (1 - y_pred) # ∂sigmoid/∂z = sigmoid(z)*(1-sigmoid(z)) dz_dW = x # ∂z/∂W = x (因为 z = W@x + b,对 W 求导得 x) # 注意:W 是 (1,2),x 是 (2,),所以 dz_dW 需要 reshape 为 (1,2) 才能广播 dloss_dW = dloss_dy * dy_dz * x.reshape(1, -1) # (1,) * (1,) * (1,2) → (1,2) print(f"手动计算 ∂loss/∂W: {dloss_dW}") # [[-0.075, -0.15]]这个手动推导过程,就是loss.backward()在后台做的全部事情。区别只在于:PyTorch 把每一步计算都记录成计算图节点,自动完成链式传递;而你用手算,是为了建立肌肉记忆——下次看到grad_W异常大,你会立刻反应:“是不是dy_dz(sigmoid 导数)太小了?是不是z太大导致 sigmoid 饱和了?” 这种条件反射,比死记“梯度消失”四个字有用十倍。
3. 为什么别跳过“概率与统计”这章?因为它决定了你能不能看懂模型在“胡说八道”还是“谨慎表态”
很多新手以为:只要模型准确率高,概率输出就一定靠谱。但现实是,一个准确率 95% 的分类器,可能对错误预测给出 0.99 的置信度——这说明它不是“强”,而是“狂”。而识别这种“狂”,靠的就是第 2、3、4 章讲的概率与统计工具。它们不是让你去算 p-value,而是给你三把尺子:量不确定性、量偏差、量数据质量。
3.1 用“校准曲线”一眼识破模型的“过度自信”
模型输出的概率值,应该和实际频率一致。比如,模型说“这张图是猫的概率是 0.7”,那么在所有被它预测为“猫且概率在 0.65~0.75 区间”的图片中,真正是猫的比例,应该接近 70%。如果实际只有 40%,说明模型“过度自信”。
书中第 3 章的“贝叶斯更新”思想,直接对应这个场景。我们可以用 NumPy + Matplotlib 快速画出校准曲线:
import numpy as np import matplotlib.pyplot as plt # 模拟模型预测(1000 个样本) np.random.seed(42) logits = np.random.randn(1000, 2) # 二分类 probs = 1 / (1 + np.exp(-logits[:, 0])) # 取第一类概率 # 人为制造“过度自信”:把概率往 0 和 1 拉 probs_calibrated = np.where(probs < 0.5, probs**2, 1 - (1-probs)**2) # 生成真实标签(假设模型本身有 85% 准确率) y_true = (probs > 0.5).astype(int) # 加入噪声,让准确率≈85% y_true = np.where(np.random.rand(1000) < 0.85, y_true, 1-y_true) # 分桶:把概率分成 10 个区间 bins = np.linspace(0, 1, 11) bin_indices = np.digitize(probs_calibrated, bins) - 1 bin_indices = np.clip(bin_indices, 0, 9) # 保证索引在 [0,9] # 计算每个桶内的平均预测概率和实际准确率 bin_avg_prob = np.array([probs_calibrated[bin_indices==i].mean() if np.any(bin_indices==i) else np.nan for i in range(10)]) bin_acc = np.array([(y_true[bin_indices==i] == (probs_calibrated[bin_indices==i] > 0.5)).mean() if np.any(bin_indices==i) else np.nan for i in range(10)]) # 画图 plt.figure(figsize=(8, 6)) plt.plot(bin_avg_prob, bin_acc, 'o-', label='模型校准曲线') plt.plot([0, 1], [0, 1], 'k--', label='理想校准线') plt.xlabel('平均预测概率') plt.ylabel('实际准确率') plt.title('模型校准诊断图') plt.legend() plt.grid(True) plt.show()注意:如果曲线整体在对角线上方,说明模型“保守”(预测 0.6 却有 0.8 准确率);如果在下方,就是“狂妄”(预测 0.8 却只有 0.5 准确率)。这个图不需要你懂贝叶斯定理,但你需要知道:概率输出不是装饰,而是模型对自己判断的“信用评级”。而这本书第 3 章的“贝叶斯更新”案例(如癌症检测那个经典例子),就是在教你怎么用先验+似然→后验的思维,去质疑一个看似高概率的预测是否真的站得住脚。
3.2 “缺失数据”不是删掉就完事,而是你数据管道的第一个漏洞探测器
你在加载数据时,是不是经常遇到NaN?然后习惯性df.dropna()?但书中第 4 章明确指出:缺失不是随机事件,而是数据生成过程的故障信号。比如,在医疗图像分类中,某类病灶的标注缺失,可能意味着该类样本本身就难标注(医生分歧大);在电商推荐中,用户行为缺失,可能代表用户根本没看到商品(曝光不足),而非不感兴趣。
我们可以用书中提到的“缺失模式分析”快速诊断:
import pandas as pd import numpy as np # 模拟一个带缺失的数据集(特征 A,B,C,目标 label) np.random.seed(42) df = pd.DataFrame({ 'A': np.random.randn(1000), 'B': np.random.randn(1000), 'C': np.random.randn(1000), 'label': np.random.choice([0,1], 1000) }) # 人为制造两种缺失:MCAR(完全随机)和 MAR(依赖其他变量) # MCAR: 5% 的 A 值随机缺失 df.loc[np.random.choice(df.index, size=50, replace=False), 'A'] = np.nan # MAR: 当 B > 0 时,C 更容易缺失(比如 B 是设备型号,B>0 表示新设备,新设备传感器易故障) mask_mar = (df['B'] > 0) & (np.random.rand(len(df)) < 0.3) df.loc[mask_mar, 'C'] = np.nan # 查看缺失模式 print("各列缺失率:") print(df.isnull().mean()) print("\n缺失相关性(A 缺失是否与 B/C 相关):") print(df.isnull().corr()['A'].round(3))输出会显示:A的缺失与B、C几乎无关(≈0),符合 MCAR;而C的缺失与B显著正相关(>0.2)。这意味着:
- 对
A,可以直接删除或均值填充(影响小); - 对
C,必须建模B和C的关系,用回归插补,否则会引入偏差。
这就是书中第 4 章“缺失数据”节的核心:处理缺失的方式,取决于你对数据生成机制的理解,而不是缺失率高低。而这种理解,正是数学建模思维的起点。
3.3 相关性 ≠ 因果性,但“相关性检验”是你拒绝垃圾特征的第一道闸门
你是不是加过一堆“听起来很酷”的特征:用户注册时长、页面停留秒数、鼠标移动轨迹……然后发现模型性能毫无提升,甚至下降?很可能,这些特征和目标变量之间,根本没有统计意义上的关联。
书中第 4 章的“Pearson 相关系数”和“Spearman 秩相关”,就是帮你快速筛掉无效特征的筛子:
from scipy.stats import pearsonr, spearmanr # 模拟特征 X 和目标 y np.random.seed(42) X = np.random.randn(1000) # y 与 X 有线性关系 + 噪声 y_linear = 2 * X + np.random.randn(1000) * 0.5 # y 与 X 有单调关系(但非线性) y_monotonic = X**3 + np.random.randn(1000) * 0.5 # 计算相关性 r_pearson, p_pearson = pearsonr(X, y_linear) r_spearman, p_spearman = spearmanr(X, y_monotonic) print(f"线性关系 Pearson r={r_pearson:.3f}, p={p_pearson:.3f}") print(f"单调关系 Spearman r={r_spearman:.3f}, p={p_spearman:.3f}") # 如果 p > 0.05,说明在 95% 置信度下,无法拒绝“无相关性”的原假设 # 这时,无论 r 看起来多大,都建议先别用这个特征提示:Pearson 检验线性相关,Spearman 检验单调相关。如果你的特征和目标是“随着特征增大,目标也增大(或减小)”,但关系是曲线形的,Spearman 会比 Pearson 更敏感。而书中强调:p-value 是你和数据之间的“谈判底线”——低于 0.05,数据才勉强同意跟你合作;高于它,就该怀疑是不是特征工程方向错了。
4. 避坑:新手在自学数学时最容易踩的 4 个“静音炸弹”
这些坑不会立刻报错,但会让你学了 20 小时,回头一看——啥也没留下。它们隐蔽、安静,却足以让自学半途而废。我第一次啃这本书时,就在第 3 个坑里卡了整整一周。
4.1 现象:抄完书上的 NumPy 代码,运行结果和书上不一样
原因:忽略了随机种子(np.random.seed())和浮点精度。书中示例为了演示清晰,常使用固定 seed 或简化计算(如1/3写成0.333),但你的环境默认 seed 是随机的,且0.1 + 0.2 != 0.3是 IEEE 754 浮点标准的必然结果。
解决:所有涉及随机数的代码,开头加np.random.seed(42);所有比较浮点数,用np.allclose(a, b, atol=1e-8)而非a == b。例如:
# ❌ 危险比较 a = 0.1 + 0.2 b = 0.3 print(a == b) # False! # ✅ 安全比较 print(np.allclose(a, b)) # True4.2 现象:看懂了公式,但写不出对应的 NumPy 代码
原因:混淆了“数学符号”和“编程维度”。比如书中写∂L/∂W = (∂L/∂Y) @ X^T,你照抄dL_dW = dL_dY @ X.T,却发现 shape 不对。这是因为数学公式省略了批量维度,而 NumPy 数组必须显式处理 batch。
解决:永远先写 shape,再写运算。例如:
X是(N, D_in),W是(D_in, D_out)→Y = X @ W是(N, D_out)dL_dY是(N, D_out),要得到dL_dW(D_in, D_out),必须是X.T @ dL_dY((D_in, N) @ (N, D_out))
动手前,在纸上画三个矩形,标好维度,再填运算符。
4.3 现象:跟着书推导完反向传播,但 PyTorch 的grad和你手算的对不上
原因:PyTorch 默认开启requires_grad=True的张量,其梯度是累积的(grad += ...),而不是覆盖的。如果你多次调用loss.backward()而没有optimizer.zero_grad()或param.grad.zero_(),梯度会越积越大。
解决:每次backward()前,确保梯度清零。最简单方式:
# ✅ 推荐:在训练循环开头清零 for epoch in range(10): optimizer.zero_grad() # 清零所有参数的 grad loss = model(x) loss.backward() optimizer.step() # ✅ 或手动清零单个参数 W.grad.zero_()4.4 现象:学完“主成分分析(PCA)”,但不知道它和你正在用的sklearn.decomposition.PCA有什么关系
原因:把 PCA 当成一个黑盒 API,而忽略了书中第 6 章强调的“几何本质”:PCA 是在找数据方差最大的正交方向(即协方差矩阵的特征向量)。sklearn.PCA的components_属性,就是这些方向组成的矩阵。
解决:用 NumPy 手动实现一次 PCA,和 sklearn 结果对比:
from sklearn.decomposition import PCA # 生成数据 X = np.random.randn(100, 5) X_centered = X - X.mean(axis=0) # 中心化 # 手动 PCA:计算协方差矩阵,求特征向量 cov = X_centered.T @ X_centered / (X_centered.shape[0] - 1) eigvals, eigvecs = np.linalg.eig(cov) # 按特征值降序排列 idx = np.argsort(eigvals)[::-1] eigvecs_manual = eigvecs[:, idx] # sklearn PCA pca = PCA(n_components=5) pca.fit(X) eigvecs_sklearn = pca.components_.T # components_ 是 (n_comp, n_features),转置为 (n_features, n_comp) # 比较:特征向量方向可能相反(符号不同),但应线性相关 print("手动 vs sklearn 第一主成分夹角(度):", np.degrees(np.arccos(np.abs(np.dot(eigvecs_manual[:,0], eigvecs_sklearn[:,0]))))) # 应该接近 0°这个对比会让你彻底明白:components_不是魔法,就是协方差矩阵的特征向量。下次你想降维,就不会盲目设n_components=0.95,而会先看explained_variance_ratio_,理解每个主成分解释了多少原始方差。
5. 下一步学什么?别急着啃完所有章节,先用“三步验证法”打通任督二脉
这本书有 11 章,但新手最该聚焦的,是第 5 章(线性代数)、第 7 章(微积分)、第 9-10 章(神经网络数据流与反向传播)这三块。它们构成深度学习的“最小可行知识环”:线性代数负责数据表示,微积分负责优化路径,数据流与反向传播负责落地执行。其他章节(如概率、统计)是锦上添花,而这三块是雪中送炭。我的建议是:用“三步验证法”,把这三块焊死在你的肌肉记忆里。
5.1 第一步:用 NumPy 从零实现一个“可调试”的全连接层
不要复制粘贴,一行一行敲,边敲边问“这行在数学上代表什么”:
class LinearLayer: def __init__(self, in_features, out_features): # 初始化权重:用 Xavier 初始化(书中第 5 章提过,防止梯度爆炸/消失) self.W = np.random.randn(in_features, out_features) * np.sqrt(2.0 / in_features) self.b = np.zeros(out_features) # 存储前向传播的输入,供反向传播用 self.x = None def forward(self, x): """x: (N, in_features) -> output: (N, out_features)""" self.x = x # 保存输入 return x @ self.W + self.b def backward(self, grad_output): """grad_output: (N, out_features) -> returns grad_input: (N, in_features)""" # ∂L/∂W = x.T @ grad_output self.grad_W = self.x.T @ grad_output # ∂L/∂b = sum over batch self.grad_b = grad_output.sum(axis=0) # ∂L/∂x = grad_output @ W.T grad_input = grad_output @ self.W.T return grad_input def update(self, lr): """用 SGD 更新参数""" self.W -= lr * self.grad_W self.b -= lr * self.grad_b # ✅ 验证:前向输出 shape 是否正确? layer = LinearLayer(784, 128) x = np.random.randn(32, 784) y = layer.forward(x) print("前向输出 shape:", y.shape) # (32, 128) # ✅ 验证:反向传播梯度 shape 是否匹配? grad_y = np.random.randn(32, 128) grad_x = layer.backward(grad_y) print("反向输入梯度 shape:", grad_x.shape) # (32, 784) print("权重梯度 shape:", layer.grad_W.shape) # (784, 128)关键检查点:
forward()的输出 shape 必须是(N, out_features);backward()返回的grad_inputshape 必须和输入x一致;self.grad_Wshape 必须和self.W一致。
如果任一 check 失败,立刻停下手,回到第 5 章“矩阵乘法维度规则”那一节,用纸笔画图推导。
5.2 第二步:把 PyTorch 的nn.Linear拆开,对照着看它的.weight和.bias
现在,用 PyTorch 创建一个等价层,打印参数,和你手写的LinearLayer对比:
import torch import torch.nn as nn # PyTorch 版本 torch_layer = nn.Linear(784, 128) # 注意:PyTorch 的 weight 是 (out_features, in_features),和 NumPy 习惯相反! print("PyTorch weight shape:", torch_layer.weight.shape) # (128, 784) print("PyTorch bias shape:", torch_layer.bias.shape) # (128,) # 手动层的 W 是 (784, 128),要转置才能和 PyTorch 对齐 # 这就是为什么你有时看到别人写 x @ W.T —— 是为了适配 PyTorch 的存储顺序血泪经验:PyTorch 和 NumPy 的矩阵存储顺序差异,是新手最大的静音坑。PyTorch 的
nn.Linear输入是(N, in_features),权重是(out_features, in_features),所以内部运算是input @ weight.t() + bias。而 NumPy 习惯把权重存为(in_features, out_features),所以x @ W更直观。不要强行统一,而要接受:框架设计有历史原因,你的任务是读懂它的文档,而不是改造它。每次用新框架,先查它的参数 shape 文档,比什么都重要。
5.3 第三步:用torch.autograd的grad_fn链,反向追踪一个 loss 的完整求导路径
这是打通“数学公式”和“代码执行”的最后一环。我们用一个超简网络,打印每一步的grad_fn:
import torch # 构建极简计算图 x = torch.tensor([2.0, 3.0], requires_grad=True) W = torch.tensor([[1.0, 2.0], [3.0, 4.0]], requires_grad=True) b = torch.tensor([0.5], requires_grad=True) # 前向:z = x @ W + b, y = z.sum() z = torch.matmul(x, W) + b # z.shape = (2,) y = z.sum() # y 是标量 print("z 的 grad_fn:", z.grad_fn) # <AddBackward0> print("y 的 grad_fn:", y.grad_fn) # <SumBackward0> # 反向传播 y.backward() print("x.grad:", x.grad) # tensor([4., 6.]) print("W.grad:", W.grad) # tensor([[2., 3.], [2., 3.]]) print("b.grad:", b.grad) # tensor([1.])现在,手动验证x.grad:
y = z.sum() = z[0] + z[1]→∂y/∂z = [1, 1]z = x @ W + b→∂z/∂x = W.T(因为z[i] = sum_j x[j] * W[j,i])- 所以
∂y/∂x = ∂y/∂z @ ∂z/∂x = [1,1] @ W.T = [1*1+1*3, 1*2+1*4] = [4,6]✅
这个手动验证过程,就是你在读第 7、8 章时,应该同步做的练习。不是为了考试,而是为了建立一种本能:看到任意一个 PyTorch 张量,你都能在脑中画出它的计算图,并预判backward()后grad会是什么样子。
从那以后我每次写自定义 Layer,都强制走一遍这个三步验证:
- NumPy 手写,shape 对齐;
- 和 PyTorch 参数对比,确认存储顺序;
- 用
grad_fn追踪,手动推导梯度。
哪怕只是 10 行代码,也比囫囵吞枣读完一章有效。因为深度学习的数学,不是用来“知道”的,而是用来“调试”的。当你能看着nan的梯度,立刻定位到是softmax输入太大导致exp溢出,或者lr太大让参数一步跨出凸区域——你就真正入门了。希望帮到你。
本文还有配套的精品资源,点击获取