首页/文章/ 详情

预备知识5-自动微分学习笔记

21小时前浏览3
    深度学习框架通过自动计算导数,即自动微分automatic differentiation)来加快求导。实际中,根据设计好的模型,系统会构建一个计算图computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。自动微分使系统能够随后反向传播梯度。这里,反向传播backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。计算图是深度学习框架实现自动微分的核心机制: 
  • 前向传播构建计算图,记录数据流动路径。
  • 反向传播按图回溯,通过链式法则高效计算梯度。

一、一个简单的例子

    作为一个例子,假设我们想对函数= 2xx关于列向量x求导。注意: 一个标量函数关于向量x的梯度是向量,并且与x具有相同的形状。



















import torchx = torch.arange(4.0) # tensor([0., 1., 2., 3.])# requires_grad_(True) 是一个用于启用张量(Tensor)梯度跟踪的方法。它的作用是告诉# PyTorch 需要记录该张量参与的所有操作,以便后续通过自动微分(Autograd)计算梯度。x.requires_grad_(True) # # 等价于x=torch.arange(4.0,requires_grad=True)# 函数一:y = 2*dot(x, x)y = 2*torch.dot(x, x)print(y) # tensor(28., grad_fn=<MulBackward0>)# grad_fn=<DotBackward0>是张量(Tensor)的一个属性,表示该张量是通过某个操作(这里是# 矩阵乘法或向量点积)生成的,并且系统已记录该操作的梯度计算规则,用于后续的反向传播。# 通过调用反向传播函数(backward)来自动计算y关于x每个分量的梯度y.backward()print(x.grad) # tensor([ 0.,  4.,  8., 12.])# 函数二: y = sum(x)# 在默认情况下,PyTorch会积累梯度,我们需要清除之前的值x.grad.zero_()y = x.sum()y.backward()print(x.grad) # tensor([1., 1., 1., 1.])

 二、非标量变量的反向传播

对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
gradient 参数的作用
  • 将非标量输出的梯度加权求和为标量,再反向传播。
  • 数学意义:若 y = f(x) 是非标量,则 gradient 是 ∂L/∂y(损失函数 L 对 y 的梯度),PyTorch 据此计算 ∂L/∂x = (∂y/∂x)^T · (∂L/∂y)

 非标量直接反向传播(错误)






import torchx = torch.tensor([1.02.0], requires_grad=True)y = x ** 2  # y = [1.0, 4.0]# 报错:RuntimeError: grad can be implicitly created only for scalar outputsy.backward() 

正确用法:传入 gradient 参数







# 定义梯度权重(形状需与 y 一致)gradient = torch.tensor([0.10.01])  # 假设 ∂L/∂y = [0.1, 0.01]y.backward(gradient) # 等价于先求和:L = 0.1*y1 + 0.01*y2,再反向传播
print(x.grad) # 输出: tensor([0.20000.0400])# 计算过程: ∂L/∂x1 = 0.1*2*x1 = 0.2, ∂L/∂x2 = 0.01*2*x2 = 0.04

实际应用:Jacobian 矩阵计算

若需计算完整 Jacobian 矩阵(而非加权求和),可对每个输出分量单独反向传播:












x = torch.tensor([1.02.0], requires_grad=True)y = x ** 2                 # y = [y1, y2]
# 计算 ∂y1/∂x 和 ∂y2/∂xjacobian = torch.zeros(22)for i in range(2):    y.backward(torch.tensor([1.0 if j == i else 0.0 for j in range(2)]), retain_graph=True)    jacobian[i] = x.grad    x.grad.zero_()
print(jacobian)# 输出: tensor([[2., 0.], [0., 4.]]) (即 [[∂y1/∂x1, ∂y1/∂x2], [∂y2/∂x1, ∂y2/∂x2]])

三、分离计算

在 PyTorch 中,分离计算(通过.detach() 或 torch.no_grad())是一种关键机制,用于控制梯度传播和优化内存使用。
假设y是x的函数,而z是y和x的函数。现需计算z关于x的梯度,但希望将y视为一个常数,且只考虑到x在y被计算后发挥的作用。





















import torchx = torch.arange(4.0, requires_grad=True) # tensor([0., 1., 2., 3.])# 本例只想求偏导数的和,所以传递一个1的梯度是合适的y = x * x # tensor([0., 1., 4., 9.], grad_fn=<MulBackward0>)# 等价于y.backward(torch.ones(len(x)))y.sum().backward()print(x.grad) # tensor([0., 2., 4., 6.])
x.grad.zero_()y=x*xu=y.detach()z = u*xz.backward(torch.ones_like(x))print(x.grad) # tensor([0., 1., 4., 9.])
# 由于记录了y的计算结果,我们可以随后在y上调用反向传播,# 得到y=x*x关于的x的导数,即2*x。x.grad.zero_()y.sum().backward()pprint(x.grad == 2 * x) # tensor([True, True, True, True])

四、Python控制流的梯度计算

使用自动微分的一个好处是:即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数 调用),我们仍然可以计算得到的变量的梯度。在下面的代码中,while循环的迭代次数和if语句的结果都取决于输入a的值。

















import torchdef f(a):  b = a * 2  while b.norm() < 1000:    b = b * 2  if b.sum() > 0:    c = b  else:    c = 100 * b  return c
#a = torch.randn(size=(), requires_grad=True)a = torch.tensor(-1, dtype=torch.float32, requires_grad=True)print(a)d = f(a)d.backward()print(a.grad) # tensor(102400.)
 

来源:檐苔
pythonUM控制
著作权归作者所有,欢迎分享,未经许可,不得转载
首次发布时间:2025-08-26
最近编辑:21小时前
青瓦松
硕士 签名征集中
获赞 14粉丝 1文章 45课程 0
点赞
收藏
作者推荐

线性神经网络4-softmax回归

一、分类问题标签表示方法:以不同的整数表示不同的类别,如以y ∈ {1, 2, 3}分别代表{狗, 猫, 鸡};独热编码(one‐hot encoding):独热编码是一个向量,它的分量和类别一样多。类别对 应的分量设置为1,其他所有分量设置为0。如标签定义y为三维向量,其中(1, 0, 0)对应于 “猫”、(0, 1, 0)对应于“鸡”、(0, 0, 1)对应于“狗”。二、网络架构仿射函数:数学和机器学习中的基础概念,它描述了输入与输出之间的线性关系,同时允许存在平移(截距项)。为了解决线性模型的分类问题,定义与输出一样多的仿射函数,即每个输出对应一个仿射函 数。如有4个特征和3个可能的输出类别,利用仿射函数可表达为:可简化为向量形式表达式:o = Wx + b。与线性回归一样,softmax回归也是一个单层神经网络。 由于计算每个输出o1、o2和o3取决于所有输入x1、x2、x3和x4,所以softmax回归的输出层也是全连接层。三、softmax运算softmax函数:对每个未规范化的预测求幂,再让每个求幂后的结果除以它们的总和,即:从以上定义可以看出,softmax函数能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持可导的性质。softmax运算不会改变未规范化的预测o之间的大小次序,只会确定分配给每个类别的概率。尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。因此,softmax回 归是一个线性模型(linear model)。softmax函数的导数及推导:四、交叉熵损失函数Cross-entropy(交叉熵损失函数) 交叉熵是用来评估当前训练得到的概率分布与真实分布的差异情况。 它刻画的是实际输出(概率)与期望输出(概率)的距离,也就是交叉熵的值越小,两个概率分布就越接近。常作为分类问题的损失函数。具体公式如下:其中,y_j是一个表示真实标签的长度为q的独热编码向量,即除了第j项为1其余项均为0;y ̂_j表示第j项的预测的概率值。交叉熵损失函数的导数及推导:五、分类问题采用交叉熵而不是均方差的为损失函数的解释交叉熵损失在分类任务中因其梯度高效、概率解释性强和优化稳定性,成为比 MSE 更优的选择。六、softmax函数上溢的解决方案在softmax计算之前,先从所有x_k中减去max(x_k):在减法和规范化步骤之后,可能有些x_i − max(x)具有较大的负值,exp(x_i − max(x))将有接近零的值而出现下溢(underflow),进而导致ˆy_i为零,并且使得log(ˆy_i )的值为-inf。反向传播几步后,我们可能会发现自己面对一屏幕可怕的nan结果。尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。通过将softmax和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题。七、softmax分类的实现import torchfrom torch.utils import dataimport torchvisionfrom torchvision import transformsimport matplotlib.pyplot as pltimport typingfrom matplotlib_inline import backend_inlinefrom IPython import displayimport plotShow# 定义一个累加器,实现对n个变量进行累加class Accumulator: &#39;&#39;&#39; 在n个变量上累加 &#39;&#39;&#39; def __init__(self, n:int): self.data = [0.0]*n def add(self, *args): self.data = [a+float(b) for a, b in zip(self.data, args)] def reset(self): self.data = [0.0]*len(self.data) def __getitem__(self, idx): return self.data[idx]class Animator: &#39;&#39;&#39; 在动画中绘制数据 &#39;&#39;&#39; def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale=&#39;linear&#39;, yscale=&#39;linear&#39;, fmts=(&#39;-&#39;, &#39;m--&#39;, &#39;g-.&#39;, &#39;r:&#39;), nrows=1, ncols=1, figsize=(3.5,2.5)): # 增量地绘制多条线条 if legend is None: legend = [] backend_inline.set_matplotlib_formats(&#39;svg&#39;) self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize) if nrows*ncols==1: self.axes = [self.axes, ] # 使用lambda函数捕获参数 self.configAxes = lambda: plotShow.setAxes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend) self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y): # 向图表中添加多个数据点 if not hasattr(y, &#39;__len__&#39;): y = [y] n = len(y) if not hasattr(x, &#39;__len__&#39;): x = [x]*n if not self.X: self.X = [[] for _ in range(n)] if not self.Y: self.Y = [[] for _ in range(n)] for i, (a, b) in enumerate(zip(x, y)): if a is not None and b is not None: self.X[i].append(a) self.Y[i].append(b) self.axes[0].cla() for x, y, fmt in zip(self.X, self.Y, self.fmts): self.axes[0].plot(x, y, fmt) self.configAxes() display.display(self.fig) plt.draw() plt.pause(0.001) display.clear_output(wait=True) def show(self): display.display(self.fig)def getFashionMnistLabels(labels:typing.Sequence): &#39;&#39;&#39; 返回FashionMnist数据集的文本标签 &#39;&#39;&#39; textLabels = [&#39;t-shirt&#39;, &#39;trouser&#39;, &#39;pullever&#39;, &#39;dress&#39;, &#39;coat&#39;, &#39;sandal&#39;, &#39;shirt&#39;, &#39;sneaker&#39;, &#39;bag&#39;, &#39;ankle boot&#39;] return [textLabels[int(i)] for i in labels]def showImages(imgs:list, numRows:int, numCols:int, titles:list=None, scale:float=1.5): figsize = (numCols*scale, numRows*scale) _, axes = plt.subplots(numRows, numCols, figsize=figsize) axes = axes.flatten() for i, (ax, img) in enumerate(zip(axes, imgs)): if torch.is_tensor(img): # 图片张量 ax.imshow(img.numpy()) else: # PIL图片 ax.imshow(img) ax.axes.get_xaxis().set_visible(False) ax.axes.get_yaxis().set_visible(False) if titles: ax.set_title(titles[i]) return axesdef getDataloaderWorkers()-&gt;int: &#39;&#39;&#39; 使用4个进程来读取数据 &#39;&#39;&#39; return 4# 读取数据集def loadDataFashionMnist(batchSize:int, resize=None)-&gt;tuple: &#39;&#39;&#39; 下载Fashion-MNIST数据集,然后将其加载到内存中 &#39;&#39;&#39; trans = [transforms.ToTensor()] if resize: trans.insert(0, transforms.Resize(resize)) trans = transforms.Compose(trans) mnistTrain = torchvision.datasets.FashionMNIST(root=&#39;../data&#39;, train=True, transform=trans, download=True) mnistTest = torchvision.datasets.FashionMNIST(root=&#39;../data&#39;, train=False, transform=trans, download=True) return (data.DataLoader(mnistTrain, batchSize, shuffle=True, num_workers=getDataloaderWorkers()), data.DataLoader(mnistTest, batchSize, shuffle=False, num_workers=getDataloaderWorkers()))# softmax函数def softmax(X:torch.Tensor)-&gt;torch.Tensor: &#39;&#39;&#39; 1. 对每个项求幂(使用exp); 2. 对每一行求和(小批量中每个样本是一行), 得到每个样本的规范化常数; 3. 将每一行除以其规范化常数, 确保结果的和为1。 &#39;&#39;&#39; XExp:torch.Tensor = torch.exp(X) partition:torch.Tensor = XExp.sum(1, keepdim=True) return XExp/partition# 定义分类精度def accuracy(yHat:torch.Tensor, y:torch.Tensor)-&gt;float: &#39;&#39;&#39; 计算预测正确的数量 &#39;&#39;&#39; if len(yHat.shape)&gt;1 and yHat.shape[1]&gt;1: yHat = yHat.argmax(axis=1) cmp = yHat.type(y.dtype)==y return float(cmp.type(y.dtype).sum())def evaluateAccuracy(net:torch.nn.Module, dataIter:typing.Tuple[torch.Tensor])-&gt;float: &#39;&#39;&#39; 计算在指定数据集上模型的精度 &#39;&#39;&#39; if isinstance(net, torch.nn.Module): net.eval() # 将模型设置为评估模式 metric = Accumulator(2) # 统计正确预测数、预测总数 with torch.no_grad(): for X, y in dataIter: metric.add(accuracy(net(X), y), y.numel()) return metric[0]/metric[1]# 定义训练模型def trainEpochCh3(net:torch.nn.Module, trainIter:typing.Tuple[torch.Tensor], loss:typing.Callable[[torch.Tensor, torch.Tensor], torch.Tensor], updater:torch.optim.Optimizer)-&gt;typing.Tuple[float]: &#39;&#39;&#39; 训练模型一个周期 &#39;&#39;&#39; # 将模型设置为训练模型 if isinstance(net, torch.nn.Module): net.train() # 训练损失总和、训练准确度总和、样本数 metric = Accumulator(3) for X, y in trainIter: yHat:torch.Tensor = net(X) l:torch.Tensor = loss(yHat, y) if isinstance(updater, torch.optim.Optimizer): # 使用PyTorch内置的优化器和损失函数 updater.zero_grad() l.mean().backward() updater.step() else: # 使用定制的优化器和损失函数 l.sum().backward() updater(X.shape[0]) metric.add(float(l.sum()), accuracy(yHat, y), y.numel()) # 返回训练损失和训练精度 return metric[0]/metric[2], metric[1]/metric[2]def trainCh3(net:torch.nn.Module, trainIter:data.DataLoader, testIter:data.DataLoader, loss:typing.Callable[[torch.Tensor, torch.Tensor], torch.Tensor], numEpochs: int, updater: torch.optim.Optimizer)-&gt;None: &#39;&#39;&#39; 训练模型 &#39;&#39;&#39; animator = Animator(xlabel=&#39;epoch&#39;, xlim=[1, numEpochs], ylim=[0.3,0.9], legend=[&#39;train loss&#39;, &#39;train acc&#39;, &#39;test acc&#39;]) for epoch in range(numEpochs): trainMetrics = trainEpochCh3(net, trainIter, loss, updater) testAcc = evaluateAccuracy(net, testIter) animator.add(epoch+1, trainMetrics+(testAcc, )) trainLoss, trainAcc = trainMetrics assert trainLoss&lt;0.5, trainLoss assert trainAcc&lt;=1 and trainAcc&gt;0.7, trainAcc assert testAcc&lt;=1 and testAcc&gt;0.7, testAcc# 定义优化算法def SGD(params:list[torch.Tensor], lr: float, batchSize:int): &#39;&#39;&#39; 小批量随机梯度下降法 params: 模型参数集 合 lr: 学习率,确定每一步更新的大小 batchSize: 批量样本大小 &#39;&#39;&#39; with torch.no_grad(): for param in params: param -= lr*param.grad/batchSize param.grad.zero_()# 预测def predictCh3(net, testIter, n=6): &#39;&#39;&#39; 预测标签 &#39;&#39;&#39; for X, y in testIter: break trues = getFashionMnistLabels(y) preds = getFashionMnistLabels(net(X).argmax(axis=1)) titles = [true + &#39;\n&#39; + pred for true, pred in zip(trues, preds)] showImages(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n]) plt.show()# softmax回归的从零开始实现if __name__ == &#39;__main__&#39;: # 1. 读取数据集 batchSize: int = 256 trainIter, testIter = loadDataFashionMnist(batchSize) # 2. 模型参数初始化 numInputs: int = 784 # 原始数据集每个样本是28*28的图像,因此以构造一个长度为784的向量来表示每一个像素点 numOutPuts: int = 10 # 数据集中有10个类别 W = torch.normal(0.0, 0.01, size=(numInputs, numOutPuts), requires_grad=True) # 权重项 b = torch.zeros(numOutPuts, requires_grad=True) # 偏置项 # 3. 定义softmax模型 def net(X:torch.Tensor)-&gt;torch.Tensor: return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b) # 4. 定义损失函数 def crossEntropy(yHat:torch.Tensor, y:torch.Tensor)-&gt;torch.Tensor: &#39;&#39;&#39; yHat: 预测概率 y: 标签向量 &#39;&#39;&#39; return -torch.log(yHat[range(len(yHat)), y]) # 5. 定义优化函数 lr = 0.1 def updater(batchSize:int): return SGD([W,b], lr, batchSize) # 6. 模型训练 numEpochs:int = 10 trainCh3(net, trainIter, testIter, crossEntropy, numEpochs, updater) # 7. 预测 predictCh3(net, testIter, n=10)# softmax回归的简洁实现if __name__ == &#39;__main__&#39;: # 1. 读取数据集 batchSize:int = 256 trainIter, testIter = loadDataFashionMnist(batchSize) # 2. 初始化模型参数 # PyTorch不会隐式地调整输入的形状。因此, # 我们在线性层前定义了展平层(flatten),来调整网络输入的形状 net = torch.nn.Sequential(torch.nn.Flatten(), torch.nn.Linear(784, 10)) def initWeight(m): if type(m)==torch.nn.Linear: torch.nn.init.normal_(m.weight, std=0.01) net.apply(initWeight) # 3. 定义损失函数 loss = torch.nn.CrossEntropyLoss(reduction=&#39;none&#39;) # 4. 定义优化算法 trainer = torch.optim.SGD(net.parameters(), lr=0.1) # 5. 训练 numEpochs:int = 10 trainCh3(net, trainIter, testIter, loss, numEpochs, trainer)来源:檐苔

未登录
还没有评论
课程
培训
服务
行家
VIP会员 学习计划 福利任务
下载APP
联系我们
帮助与反馈