系列:《从小白到 AI 大模型开发工程师的进阶之路》 技术点:AI-0217 链式法则 主人公:小蓝伞 前置:AI-0216 导数与偏导数 本期产出:独立实验脚本、原始输出、失败样本和验收清单
小蓝伞在教学两层网络中保留了线性残差,却在反向时只处理非线性路径,最终输入梯度恰好少了一。这个人工构造的错误能被手算和差分同时抓住。 本期按准备环境、运行完整脚本、读取结果和复验失败的顺序完成实践。复合路径上的局部导数相乘,多条路径通往同一变量时贡献相加。前向缓存确保反向使用同一次计算的中间值,梯度累积确保共享变量不丢分支。 输入均为教学合成数据,不是生产经历。
一、小蓝伞遇到的问题
本例输入先乘第一层权重再加偏置,经双曲正切激活后乘第二层权重,最后加回原始输入。输入到输出有两条路径:非线性变换和直接残差。前向得到约一点三零六五五一六七。只有把每个节点、输入和输出定义清楚,才能讨论反向计算;对网络名称背公式却漏掉残差边,会得到结构性错误。
二、先给结论
复合路径上的局部导数相乘,多条路径通往同一变量时贡献相加。前向缓存确保反向使用同一次计算的中间值,梯度累积确保共享变量不丢分支。
推荐:把前向和反向定义分开,缓存必要状态;共享节点用累加;用小图手算和中心差分做独立复验。
不推荐:省略输入语义和指标口径,仅凭单次输出扩大结论。
成功判据:手算输入梯度与中心差分绝对差小于1e-8;漏分支反例的差值应为1;零输入时对应权重梯度为0。
三、任务与数据契约
| 项目 | 本期约定 |
|---|---|
| 输入 | 完整代码构造的合成数据 |
| 输出 | 数值、图表或异常及原始日志 |
| 环境 | Windows 11、Python 3.13.9,依赖见记录 |
| 不在范围 | 真实业务、生产容量和跨设备性能 |
四、前置准备
在新目录保存工作目录中的实验.py为main.py,再执行:
python -m venv .venv .\.venv\Scripts\python.exe -m pip install numpy==2.3.5 .\.venv\Scripts\python.exe main.py
本次实际执行使用 Windows 11、Python 3.13.9;代码只生成合成输入,不依赖账号、令牌或隐藏文件。
五、核心原理
计算图:明确记录中间运算和依赖的图。
局部导数:单个节点输出对其输入的变化率。
链式法则:沿复合路径组合局部导数。
梯度累积:把同一变量的多路贡献相加。
残差分支:输入绕过部分变换直接到输出的路径。
路径乘积与汇合求和
非线性路径的输入导数是第二层权重、激活局部导数和第一层权重的乘积;直接残差路径的输入导数为一。两者相加得到约二点九零四二一八七七。漏掉残差贡献得到约一点九零四二一八七七,差值恰为一。这个差值不是浮点噪声,而是图中的一条边没有参与反向。
共享节点为什么不能覆盖
当一个变量参与多个后续运算,每条边都对最终目标有贡献。反向遍历到该变量时应累加贡献,赋值覆盖只留下最后访问的路径。访问顺序变化可能改变错误结果,使故障看起来像随机抖动。明确初始化梯度、每轮是否清零、何时累积批次,比修补某个结果数值更可靠。
用有限差分建立独立检查
本例左右扰动输入百万分之一,前向两次得到中心差分,和手工反向结果在八位小数显示一致。零输入时第一层权重梯度为零,是因为它乘输入,不代表整个网络所有梯度为零。应对输入、权重和偏置分别设置检查,而不是用一条输入梯度验证整个训练器。
六、完整项目
完整脚本位于_work/第63期-链式法则/实验.py。它包含正常、边界和失败断言;复制到main.py后可独立运行。
import numpy as np def forward(x,w,b,v): """缓存两层网络的预激活和激活,保留残差分支。""" z=w*x+b h=np.tanh(z) y=v*h+x return y,(x,w,v,h) def backward(cache): """两条通往输入的路径累加,返回输入和第一层参数梯度。""" x,w,v,h=cache dz=v*(1-h*h) return dz*w+1,dz*x,dz x,w,b,v=.4,2.,-.1,1.5 value,cache=forward(x,w,b,v) dx,dw,db=backward(cache) epsilon=1e-6 numeric=(forward(x+epsilon,w,b,v)[0]-forward(x-epsilon,w,b,v)[0])/(2*epsilon) print(f"output={value:.8f} dx={dx:.8f} numeric={numeric:.8f} dw={dw:.8f} db={db:.8f}") print(f"漏残差分支={dx-1:.8f} 与正确值差={1.0:.8f}") assert abs(dx-numeric)<1e-8 _,zero_cache=forward(0.,w,b,v) assert backward(zero_cache)[1]==0 print("零输入参数梯度",backward(zero_cache)[1],"正常/边界断言通过") 实现与结果的进一步核验
手算两层参数梯度
非线性输出对激活的导数为第二层权重,激活对预激活的导数为一减激活平方。预激活对第一层权重的导数为输入,对偏置为一。相乘得到本例第一层权重梯度约零点三八零八四三七五、偏置梯度约零点九五二一零九三八。输入梯度还要加残差一,这个区别说明不同参数拥有不同依赖路径。
读代码时按状态而非行数
先运行前向取得输出和缓存,再把同一缓存传入反向。缓存元组中不必保存可从其他量稳定推得的一切对象,但要保存反向所需的状态。若前向之后修改权重再求反向,就不再是同一函数点的导数。即使输出数字变化不大,梯度检查仍可能失败,因此缓存的时点比变量名更关键。
累积与清零的两种任务
共享节点的多路贡献在同一次反向必须累加;跨批次是否累积则属于训练策略。普通每批更新通常先清上批梯度,梯度累积训练则刻意保留若干批的贡献并处理尺度。把两种累积混为一谈,容易用每次清零修坏共享节点,或用永久不清零放大跨轮梯度。本文仅展示一次图的内部累积,不实现训练循环。
手算与差分怎样双向定位
手算正确、差分正确但程序错误时,优先检查反向实现;手算和程序一致却差分不稳时,检查步长与前向状态;三者不一致时,先把图缩小到一个节点。残差反例差值恰为一是结构证据,比笼统说误差较大更能定位漏边。不要通过修改预期数字来让断言通过。
扩到向量时哪些约定变化
标量网络中的乘法可直接看局部导数,向量权重需要转置、广播和批次聚合约定。共享参数跨样本出现时,梯度求和或平均由损失定义决定,不由代码方便决定。先用本例理解路径,再用下期Jacobian明确输出与输入轴,能减少形状正确但梯度尺度错误的静默故障。
七、可复现失败案例
人工漏掉直接残差路径后输入梯度少1;零输入边界下权重梯度应为0。 这是构造的教学失败,不是线上事故。复现顺序是保存输入、运行脚本、记录输出、只改一个条件后重跑。修复后必须同时检查正常样本,不能用默认值掩盖错误。
八、实验设计与实际结果
手算输入梯度与中心差分绝对差小于1e-8;漏分支反例的差值应为1;零输入时对应权重梯度为0。 本次已经执行,退出状态为0;完整输出见工作目录。控制变量、样本量和参数写在脚本中。
output=1.30655167 dx=2.90421877 numeric=2.90421877 dw=0.38084375 db=0.95210938 漏残差分支=1.90421877 与正确值差=1.00000000 零输入参数梯度 0.0 正常/边界断言通过
数字怎样对应本期机制
本例缓存输入、第一层权重、第二层权重和激活值。反向可以从激活值计算局部导数,避免重新执行前向。大型网络的缓存会占内存,重计算或检查点策略则交换时间和空间;无论如何必须保证重计算使用同一参数、同一随机条件和同一数据。缓存不是随便保存几个数,而是前向状态的可追溯快照。
九、常见问题与排查顺序
先检查对象定义,再检查输入、形状、分母、索引或时间点,最后检查实现与环境。双曲正切在大绝对值处局部导数很小,这是函数本身的行为;漏掉分支则是实现错误。两者都可能表现为梯度变小,但排查方法不同:前者检查预激活范围,后者检查图路径和累积。本期只处理标量两层结构,不承诺训练收敛,下一期会把形状扩大到向量输出的Jacobian。 不要把一次成功当成稳定性证明,也不要把技术指标直接换成业务价值。
十、系统与依赖差异
本次只在记录的 Windows 和库版本运行。浮点末位、计时、字体、编码和表格默认行为可能跨环境变化;未执行部分保持待核验。
十一、验证清单
执行脚本,核对退出状态与本篇实际输出。
重跑本篇正常、边界和失败样本,检查数值或拒绝结果。
更换数据时重新检查对象、分母和评价条件。
验收判据:手算输入梯度与中心差分绝对差小于1e-8;漏分支反例的差值应为1;零输入时对应权重梯度为0。
十二、面试题与追问
计算图在本期是什么?
答:明确记录中间运算和依赖的图。 本例输入先乘第一层权重再加偏置,经双曲正切激活后乘第二层权重,最后加回原始输入。输入到输出有两条路径:非线性变换和直接残差。前向得到约一点三零六五五一六七。只有把每个节点、输入和输出定义清楚,才能讨论反向计算;对网络名称背公式却漏掉残差边,会得到结构性错误。
追问:改变这个条件后,哪一个输出或失败判据应该变化?
局部导数在本期是什么?
答:单个节点输出对其输入的变化率。 非线性路径的输入导数是第二层权重、激活局部导数和第一层权重的乘积;直接残差路径的输入导数为一。两者相加得到约二点九零四二一八七七。漏掉残差贡献得到约一点九零四二一八七七,差值恰为一。这个差值不是浮点噪声,而是图中的一条边没有参与反向。
追问:改变这个条件后,哪一个输出或失败判据应该变化?
链式法则在本期是什么?
答:沿复合路径组合局部导数。 本例缓存输入、第一层权重、第二层权重和激活值。反向可以从激活值计算局部导数,避免重新执行前向。大型网络的缓存会占内存,重计算或检查点策略则交换时间和空间;无论如何必须保证重计算使用同一参数、同一随机条件和同一数据。缓存不是随便保存几个数,而是前向状态的可追溯快照。
追问:改变这个条件后,哪一个输出或失败判据应该变化?
梯度累积在本期是什么?
答:把同一变量的多路贡献相加。 当一个变量参与多个后续运算,每条边都对最终目标有贡献。反向遍历到该变量时应累加贡献,赋值覆盖只留下最后访问的路径。访问顺序变化可能改变错误结果,使故障看起来像随机抖动。明确初始化梯度、每轮是否清零、何时累积批次,比修补某个结果数值更可靠。
追问:改变这个条件后,哪一个输出或失败判据应该变化?
残差分支在本期是什么?
答:输入绕过部分变换直接到输出的路径。 本例左右扰动输入百万分之一,前向两次得到中心差分,和手工反向结果在八位小数显示一致。零输入时第一层权重梯度为零,是因为它乘输入,不代表整个网络所有梯度为零。应对输入、权重和偏置分别设置检查,而不是用一条输入梯度验证整个训练器。
追问:改变这个条件后,哪一个输出或失败判据应该变化?
十三、工程判断与下一期
把前向和反向定义分开,缓存必要状态;共享节点用累加;用小图手算和中心差分做独立复验。 下一期为AI-0218 梯度与 Jacobian。本期保留的契约和反例会继续使用。
从本地证据到实际决策
损失节点还会增加一层链
本篇输出本身被当作标量求导。训练时通常还有预测到损失的映射,必须乘上损失对输出的导数。若平方损失取平均,批次数还影响尺度;不能把网络输出梯度直接当训练参数梯度。先把目标节点写出,才知道反向的种子应是什么,框架默认行为也需要这个标量条件。
从节点级断言到整体断言
先核对加法局部导数、乘法两个输入导数、激活函数导数,再组合整个网络。整体错误可能由两个局部错误抵消,单个成功点不足以覆盖实现。可以选择激活接近零、正负输入和共享变量的小例,报告哪些点真的运行过。本篇保存的零输入只检查对应权重梯度,不冒称覆盖所有激活边界。
更新时机为什么重要
反向得出梯度之后才更新参数,前向缓存不应继续作为新参数点的缓存。若为了省代码交替修改权重、计算下一导数,链式乘积可能混用新旧状态。明确前向、反向和更新三个阶段,有利于日志、恢复和独立校验。训练系统并发或梯度累积时,阶段边界尤其需要约定。
本例残差的误判如何修复
漏掉直接输入贡献时误差恒差一,小蓝伞可以先暂时关闭残差,再看非线性路径是否单独匹配差分。若单路径通过、双路径失败,优先检查汇合加法节点和累积。修复后恢复原图,再用原输入、零输入和差分重复检查,不能只改预期数值。这个排查顺序把图结构当证据,而不是盲调学习率。
迁移到自动微分框架的范围
框架负责基本算子的求导与图遍历,用户仍负责定义损失、控制参数状态和选择需要梯度的变量。脱离图的转换、原地写入和不支持算子都可能改变路径。本文只演示手写标量图,没有证明任意框架配置正确;下期将以最小双数展示自动微分如何组织多个方向。
官方资料
A Gentle Introduction to torch.autograd — PyTorch Tutorials 2.14.0+cu130 documentation:用于核对本期技术机制或接口,查阅日期为2026-10-13。
适用边界
只覆盖脚本中的合成数据、明确假设和已记录版本。真实业务需追加授权、监控、资源预算、回滚和领域验证。