参考来源:
CSDN:理解optimizer.zero_grad(), loss.backward(), optimizer.step()的作用及原理
博客园:Autograd: 自动求导
在用 pytorch 训练模型时,通常会在遍历 epochs 的过程中依次用到 **optimizer.zero_grad()**、**loss.backward()** 和 **optimizer.step()** 三个函数,如下所示:
model = MyModel()criterion = nn.CrossEntropyLoss()optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4)for epoch in range(1, epochs):for i, (inputs, labels) in enumerate(train_loader):output= model(inputs)loss = criterion(output, labels)# compute gradient and do SGD stepoptimizer.zero_grad()loss.backward()optimizer.step()
总得来说,这三个函数的作用是先将梯度归零(**optimizer.zero_grad()**),然后反向传播计算得到每个参数的梯度值(**loss.backward()**),最后通过梯度下降执行一步参数更新(**optimizer.step()****)
**
接下来将通过源码分别理解这三个函数的具体实现过程。在此之前,先简要说明一下函数中常见的参数变量:
**param_groups**:Optimizer 类在实例化时会在构造函数中创建一个 param_groups 列表,列表中有 num_groups 个长度为 6 的 param_group 字典(num_groups 取决于你定义 optimizer 时传入了几组参数),每个 param_group 包含了 ['params', 'lr', 'momentum', 'dampening', 'weight_decay', 'nesterov'] 这 6 组键值对。**param_group['params']**:由传入的模型参数组成的列表,即实例化 Optimizer 类时传入该 group 的参数,如果参数没有分组,则为整个模型的参数 model.parameters(),每个参数是一个 torch.nn.parameter.Parameter 对象。
1. optimizer.zero_grad():
def zero_grad(self):r"""Clears the gradients of all optimized :class:`torch.Tensor` s."""for group in self.param_groups:for p in group['params']:if p.grad is not None:p.grad.detach_()p.grad.zero_()
optimizer.zero_grad() 函数会遍历模型的所有参数,通过 p.grad.detach_() 方法截断反向传播的梯度流,再通过 p.grad.zero_() 函数将每个参数的梯度值设为 0,即上一次的梯度记录被清空。
因为训练的过程通常使用 **mini-batch** 方法,所以如果不将梯度清零的话,梯度会与上一个 batch 的数据相关,因此该函数要写在反向传播和梯度下降之前。
2. loss.backward():
PyTorch 的反向传播(即 tensor.backward())是通过 autograd 包来实现的,autograd 包会根据 tensor 进行过的数学运算来自动计算其对应的梯度。
具体来说,torch.tensor 是 autograd 包的基础类,如果你设置 tensor 的 requires_grads 为 True,就会开始跟踪这个 tensor 上面的所有运算,如果你做完运算后使用 **tensor.backward()** ,所有的梯度就会自动运算, **tensor** 的梯度将会累加到它的 **.grad** 属性里面去。
更具体地说,损失函数 loss 是由模型的所有权重 w 经过一系列运算得到的,若某个 w 的 requires_grads 为 True,则 w 的所有上层参数(后面层的权重 w)的 .grad_fn 属性中就保存了对应的运算,然后在使用 loss.backward() 后,会一层层的反向传播计算每个 w 的梯度值,并保存到该 w 的 .grad 属性中。
如果没有进行 **tensor.backward()** 的话,梯度值将会是 **None**,因此 **loss.backward()** 要写在 **optimizer.step()** 之前。
3. optimizer.step():
以 SGD 为例,torch.optim.SGD().step() 源码如下:
def step(self, closure=None):"""Performs a single optimization step.Arguments:closure (callable, optional): A closure that reevaluates the modeland returns the loss."""loss = Noneif closure is not None:loss = closure()for group in self.param_groups:weight_decay = group['weight_decay']momentum = group['momentum']dampening = group['dampening']nesterov = group['nesterov']for p in group['params']:if p.grad is None:continued_p = p.grad.dataif weight_decay != 0:d_p.add_(weight_decay, p.data)if momentum != 0:param_state = self.state[p]if 'momentum_buffer' not in param_state:buf = param_state['momentum_buffer'] = torch.clone(d_p).detach()else:buf = param_state['momentum_buffer']buf.mul_(momentum).add_(1 - dampening, d_p)if nesterov:d_p = d_p.add(momentum, buf)else:d_p = bufp.data.add_(-group['lr'], d_p)return loss
step() 函数的作用是执行一次优化步骤,通过梯度下降法来更新参数的值。因为梯度下降是基于梯度的,所以在执行 optimizer.step() 函数前应先执行 loss.backward() 函数来计算梯度。
注意:optimizer 只负责通过梯度下降进行优化,而不负责产生梯度,梯度是 tensor.backward() 方法产生的。
