当前位置: 首页 > news >正文

AI的工程基础2-反向传播

数据结构定义-标量scalar

class Scalar: def __init__(self, value, prevs=[], op=None, label='', requires_grad=True): # 节点的值 self.value = value # 节点的标识(label)和对应的运算(op),用于作图 self.label = label self.op = op # 节点的前节点,即当前节点是运算的结果,而前节点是参与运算的量 self.prevs = prevs # 是否需要计算该节点偏导数,即∂loss/∂self(loss表示最后的模型损失) self.requires_grad = requires_grad # 该节点偏导数,即∂loss/∂self self.grad = 0.0 # 如果该节点的prevs非空,存储所有的∂self/∂prev self.grad_wrt = dict() # 作图需要,实际上对计算没有作用 self.back_prop = dict() def __repr__(self): return f'Scalar(value={self.value:.2f}, grad={self.grad:.2f})' def __add__(self, other): ''' 定义加法,self + other将触发该函数 ''' if not isinstance(other, Scalar): other = Scalar(other, requires_grad=False) # output = self + other output = Scalar(self.value + other.value, [self, other], '+') output.requires_grad = self.requires_grad or other.requires_grad # 计算偏导数 ∂output/∂self = 1 output.grad_wrt[self] = 1 # 计算偏导数 ∂output/∂other = 1 output.grad_wrt[other] = 1 return output def __sub__(self, other): ''' 定义减法,self - other将触发该函数 ''' if not isinstance(other, Scalar): other = Scalar(other, requires_grad=False) # output = self - other output = Scalar(self.value - other.value, [self, other], '-') output.requires_grad = self.requires_grad or other.requires_grad # 计算偏导数 ∂output/∂self = 1 output.grad_wrt[self] = 1 # 计算偏导数 ∂output/∂other = -1 output.grad_wrt[other] = -1 return output def __mul__(self, other): ''' 定义乘法,self * other将触发该函数 ''' if not isinstance(other, Scalar): other = Scalar(other, requires_grad=False) # output = self * other output = Scalar(self.value * other.value, [self, other], '*') output.requires_grad = self.requires_grad or other.requires_grad # 计算偏导数 ∂output/∂self = other output.grad_wrt[self] = other.value # 计算偏导数 ∂output/∂other = self output.grad_wrt[other] = self.value return output def __pow__(self, other): ''' 定义乘方,self**other将触发该函数 ''' assert isinstance(other, (int, float)) # output = self ** other output = Scalar(self.value ** other, [self], f'^{other}') output.requires_grad = self.requires_grad # 计算偏导数 ∂output/∂self = other * self**(other-1) output.grad_wrt[self] = other * self.value**(other - 1) return output def sigmoid(self): ''' 定义sigmoid ''' s = 1 / (1 + math.exp(-1 * self.value)) output = Scalar(s, [self], 'sigmoid') output.requires_grad = self.requires_grad # 计算偏导数 ∂output/∂self = output * (1 - output) output.grad_wrt[self] = s * (1 - s) return output def __rsub__(self, other): ''' 定义右减法,other - self将触发该函数 ''' if not isinstance(other, Scalar): other = Scalar(other, requires_grad=False) output = Scalar(other.value - self.value, [self, other], '-') output.requires_grad = self.requires_grad or other.requires_grad # 计算偏导数 ∂output/∂self = -1 output.grad_wrt[self] = -1 # 计算偏导数 ∂output/∂other = 1 output.grad_wrt[other] = 1 return output def __radd__(self, other): ''' 定义右加法,other + self将触发该函数 ''' return self.__add__(other) def __rmul__(self, other): ''' 定义右乘法,other * self将触发该函数 ''' return self * other def backward(self, fn=None): ''' 由当前节点出发,求解以当前节点为顶点的计算图中每个节点的偏导数,i.e. ∂self/∂node 参数 ---- fn :画图函数,如果该变量不等于None,则会返回向后传播每一步的计算的记录 返回 ---- re :向后传播每一步的计算的记录 ''' def _topological_order(): ''' 利用深度优先算法,返回计算图的拓扑排序(topological sorting) ''' def _add_prevs(node): if node not in visited: visited.add(node) for prev in node.prevs: _add_prevs(prev) ordered.append(node) ordered, visited = [], set() _add_prevs(self) return ordered def _compute_grad_of_prevs(node): ''' 由node节点出发,向后传播 ''' # 作图需要,实际上对计算没有作用 node.back_prop = dict() # 得到当前节点在计算图中的梯度。由于一个节点可以在多个计算图中出现, # 使用cg_grad记录当前计算图的梯度 dnode = cg_grad[node] # 使用node.grad记录节点的累积梯度 node.grad += dnode for prev in node.prevs: # 由于node节点的偏导数已经计算完成,可以向后扩散(反向传播) # 需要注意的是,向后扩散到上游节点是累加关系 grad_spread = dnode * node.grad_wrt[prev] cg_grad[prev] = cg_grad.get(prev, 0.0) + grad_spread node.back_prop[prev] = node.back_prop.get(prev, 0.0) + grad_spread # 当前节点的偏导数等于1,因为∂self/∂self = 1。这是反向传播算法的起点 cg_grad = {self: 1} # 为了计算每个节点的偏导数,需要使用拓扑排序的倒序来遍历计算图 ordered = reversed(_topological_order()) re = [] for node in ordered: _compute_grad_of_prevs(node) # 作图需要,实际上对计算没有作用 if fn is not None: re.append(fn(self, 'backward')) return re

自定义运算符

在编程中,__add__是一种特殊方法(或称“魔术方法”),用于定义对象的加法操作(+)。以下是关于__add__方法的详细说明和示例。

基本语法

__add__方法需要至少一个参数(self除外),通常命名为other,表示参与加法运算的另一个对象。方法必须返回加法操作的结果。

def __add__(self, other): # 实现加法逻辑 return result

数据结构说明

Scalar类是构建自动微分计算图的核心数据结构,它封装了一个标量值及其在反向传播中所需的所有信息。其内部属性与功能如下:

核心属性

value:标量节点的数值,是前向传播的计算结果。

label:节点的标识符,主要用于可视化计算图。

op:生成该节点的运算符(如'+''*''sigmoid'),用于描述节点来源。

prevs:前驱节点列表,即参与当前运算的输入节点。它定义了计算图的依赖关系。

requires_grad:布尔值,指示该节点是否需要计算梯度(即∂loss/∂self)。

grad:累积梯度值,即损失函数对该节点的偏导数∂loss/∂self。在多次反向传播中会累加。

grad_wrt:字典,键为前驱节点,值为局部偏导数∂self/∂prev。它记录了当前节点对其每个输入的导数,用于链式法则。

back_prop:字典,用于记录反向传播过程中的中间值,主要服务于可视化,不影响数值计算。

在计算图中的角色

每个Scalar实例可视作计算图中的一个节点:

叶子节点prevs为空,通常为输入变量或常数。

中间节点:由运算符(如__add____mul__)产生,其prevs指向参与运算的节点。

输出节点:计算图的最终输出,通过调用其backward()方法触发整个图的反向传播。

梯度计算与存储

为了支持反向传播,Scalar采用以下机制:

前向传播时记录局部导数:在每个运算符(如__add__)中,会计算并存储grad_wrt,即∂output/∂input

梯度累积grad属性用于累积从不同路径传播而来的梯度(因为一个节点可能被多个后续节点依赖)。

反向传播驱动:从输出节点开始,按照拓扑排序的逆序,利用grad_wrt和链式法则,将梯度逐节点向前传递。

这种设计使得Scalar既能保存数值,又能自动维护计算图的结构和梯度信息,是实现自动微分(Autograd)功能的基础单元。

简单的计算图

from ch07_autograd.utils import Scalar,draw_graph # 简单的计算图 a = Scalar(1.0, label='a') b = Scalar(2.0, label='b') c = a + b draw_graph(c) a = Scalar(1.0, label='a') b = Scalar(2.0, label='b') c = Scalar(4.0, label='c') d = a + b e = a * c f = d * e backward_process = f.backward(draw_graph) draw_graph(f) draw_graph(f, 'backward') # 将反向传播的过程展示出来(可能会有弹框) # for index, pic in enumerate(backward_process): # pic.view(str(index))

应用到线性回归

# 计算图膨胀 model = Linear() # 定义训练数据 x1 = Scalar(1.5, label='x1', requires_grad=False) y1 = Scalar(1.0, label='y1', requires_grad=False) x2 = Scalar(2.0, label='x2', requires_grad=False) y2 = Scalar(4.0, label='y2', requires_grad=False) loss = mse([model.error(x1, y1), model.error(x2, y2)]) draw_graph(loss) # 第一次触发方向传播 loss.backward() draw_graph(loss, 'backward') # 第二次触发方向传播 loss.backward() draw_graph(loss, 'backward')

完整训练代码修改

import torch # 固定随机种子,使得运行结果可以稳定复现 torch.manual_seed(1024) # 产生训练用的数据 x_origin = torch.linspace(100, 300, 200) # 将变量X归一化,否则梯度下降法很容易不稳定 x = (x_origin - torch.mean(x_origin)) / torch.std(x_origin) epsilon = torch.randn(x.shape) y = 10 * x + 5 + epsilon # 生成模型 model = Linear() # 定义每批次用到的数据量 batch_size = 20 learning_rate = 0.1 for t in range(20): # 选取当前批次的数据,用于训练模型 ix = (t * batch_size) % len(x) xx = x[ix: ix + batch_size] yy = y[ix: ix + batch_size] # 计算当前批次数据的损失 loss = mse([model.error(_x, _y) for _x, _y in zip(xx, yy)]) # 计算损失函数的梯度 loss.backward() # 迭代更新模型参数的估计值 model.a -= learning_rate * model.a.grad model.b -= learning_rate * model.b.grad # 将使用完的梯度清零 model.a.grad = 0.0 model.b.grad = 0.0 print(f'Step {t + 1}, Result: {model.string()}')
http://www.cnnetsun.cn/news/3200587.html

相关文章:

  • 炉石传说插件HsMod:终极游戏体验增强完全指南
  • 什么是 GEO?未来3年,为什么它是比SEO更重要的流量风口
  • 保姆级教程:手把手搭建MCP服务器让AI替你干活
  • 京东JoyAI-VL-Interaction:实时视觉语言交互系统从原理到实践
  • 无限制OCR技术解析:从Tesseract到分布式处理的长文档识别方案
  • 3分钟救回损坏视频!Untrunc视频修复工具的神奇魔力
  • TensorRT安装避坑指南:CUDA/cuDNN/驱动版本兼容性详解
  • Minecraft基岩版启动器BedrockLauncher:告别版本冲突,打造个性化游戏空间
  • 面向非数字化商户的对话式AI系统设计:零登录、多语言、意图路由
  • OpCore-Simplify:15分钟搞定黑苹果配置的终极简化工具
  • FR-4 PCB Tg值选型指南:3类板材(130°C/150°C/170°C+)适用场景与成本分析
  • 重连成功,图表却骗了你
  • Turbo Boost配置分享
  • CIC研判:数字化越转越重,病根还是在低代码工作流
  • RIP动态实验
  • 在Windows平台构建企业级RTMP流媒体服务器的完整指南
  • YOLOv10模型改进-特定领域应用-第93篇:YOLOv10改进策略【特定领域应用】| YOLOv10在医疗影像中的应用
  • KES是什么?国产数据库技术架构、核心能力与选型实战解析
  • PyTorch MSE Loss 维度不匹配:从 2048 vs 2088 报错到 3 步定位法
  • 【专业服务】游戏试玩广告全流程定制开发:从创意到数据,助力高效获客
  • 2026年企业上Agent的真实落地率只有17%——剩下的83%,卡在哪了?
  • 具身智能十年演进:从物理仿真到世界模型的认知跃迁
  • 6种技术文档类型,Baklib轻松搞定
  • SWEET32漏洞实战:TLS安全加固与3DES加密套件禁用指南
  • Python爬虫经典案例第83篇:在线博客平台爬取:掘金数据采集实战
  • PyTorch 2.x 张量调试:5 个 print 语句定位 90% 维度不匹配问题
  • 公司注销指南:登报清算公告怎么写?登报流程与要求详解
  • AI程序员生存指南25-技术更新太快跟不上?技术趋势跟踪的实战方法
  • 动物皮肤病识别 AI模型训练 猫狗皮肤病医疗数据集 训练用于猫狗皮肤病诊断的AI模型
  • 如何快速上手:3步掌握Windows硬件信息修改工具