神经网络的训练主要使用反向传播算法,将模型预测值(logits)与正确标签(label)送入损失函数(loss function)获得loss,然后进行反向传播计算,求得梯度(gradients),最终更新至模型参数(parameters)。自动微分能够计算可导函数在某点处的导数值,是反向传播算法的一般化。自动微分主要解决的问题是将一个复杂的数学运算分解为一系列简单的基本运算,该功能对用户屏蔽了大量的求导细节和过程,大大降低了框架的使用门槛。
MindSpore使用函数式自动微分的设计理念,提供更接近于数学语义的自动微分接口grad和value_and_grad。下面我们使用一个简单的单层线性变换模型进行介绍。
1.基本环境:导入库
import numpy as np
import mindspore
from mindspore import nn
from mindspore import ops
from mindspore import Tensor, Parameter
import time
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())), 'xxxx')
2.函数与计算图
计算图是用图论语言表达数学函数的方式,也是深度学习框架表达神经网络模型的统一方法,此处使用下边的计算图构造神经网络,其中x为输入,y为正确值, w,b是需要优化的参数:
x = ops.ones(5, mindspore.float32)
y = ops.zeros(3, mindspore.float32)
w = Parameter(Tensor(np.random.randn(5, 3), mindspore.float32)
b = Parameter(Tensor(np.random.randn(5, 3), mindspore.float32, name='b')
根据计算图描述的过程可以构造计算函数,参考下面链接。执行计算函数可以得到loss。mindspore.ops.binary_cross_entropy_with_logits — MindSpore master 文档
def function(x, y, w, b):
z = ops.matmul(x, w) + b
loss = ops.binary_cross_entropy_with_logits(z, y, ops.ones_like(z), ops.ones_like(z))
return loss
loss = function(x, y, w, b)
print(loss)
4.微分函数与梯度计算
优化模型参数是通过求loss对参数的导数来实现的:$\frac{\partial \operatorname{loss}}{\partial w}$与$\frac{\partial \operatorname{loss}}{\partial b}$。然后调用`mindspore.grad`函数获得`function`函数的导函数。
`grad`函数有两个入参,第一个fn为待求导的函数,grad_position为求导输入位置的索引,例如此处function中w,b的位置分别为2,3,因此传入参数(2,3)。
grad_fn = mindspore.grad(function,(2,3))
获取$w,b$的梯度。
grads = grad_fn(x, y, w, b)
5.Stop Gradient
通常情况下,求导时会求loss对参数的导数,因此函数的输出只有loss一项。当我们希望函数输出多项时,微分函数会求所有输出项对参数的导数。此时如果想实现对某个输出项的梯度截断,或消除某个Tensor对梯度的影响,需要用到Stop Gradient操作。
这里我们将function改为同时输出loss和z的function_with_logits,获得微分函数并执行。
def function_with_logits(x, y, w,b):
z = ops.matmul(x, w) + b
loss = ops.binary_cross_entropy_with_logits(z, y, ops.ones_like(z), ops.ones_like(z))
return loss, z
grad_fn = mindspore.grad(function_with_logits, (2, 3))
grads = grad_fn(x, y,, w, b)
print(grads)
可以看到求w,b 对应的梯度值发生了变化。此时如果想要屏蔽掉z对梯度的影响,即仍只求参数对loss的导数,可以使用ops.stop_gradient接口,将梯度在此处截断。我们将function实现加入stop_gradient,并执行。
def function_stop_gradient(x, y, w,b):
z = ops.matmul(x, w) + b
loss = ops.binary_cross_entropy_with_logits(z, y, ops.ones_like(z), ops.ones_like(z))
return loss, ops.stop_gradient(z)
grad_fn = mindspore.grad(function_stop_gradient, (2, 3))
grads = grad_fn(x, y, w, b)
print(grads)
7.辅助数据Auxiliary data
Auxiliary data意为辅助数据,是函数除第一个输出项外的其他输出。通常我们会将函数的loss设置为函数的第一个输出,其他的输出即为辅助数据。
grad和value_and_grad提供has_aux参数,当其设置为True时,可以自动实现前文手动添加stop_gradient的功能,满足返回辅助数据的同时不影响梯度计算的效果。
下面仍使用function_with_logits,配置has_aux=True,并执行
grad_fn = mindspore.grad(function_with_logits, (2, 3), has_aux = True)
grads, (z,) = grad_fn(x,y,w,b)
print(grads, z)
8.神经网络梯度计算
前述章节主要根据计算图对应的函数介绍了MindSpore的函数式自动微分,但我们的神经网络构造是继承自面向对象编程范式的nn.Cell。接下来我们通过Cell构造同样的神经网络,利用函数式自动微分来实现反向传播。
首先我们继承nn.Cell构造单层线性变换神经网络。这里我们直接使用前文的 𝑤,𝑏
作为模型参数,使用mindspore.Parameter进行包装后,作为内部属性,并在construct内实现相同的Tensor操作。
class Network(nn.Cell):
def __init__(self):
super().__init__()
self.w = w
self.b = b
def construct(self,x):
z = ops.matmul(x, self.w) + self.b
return z
实例化模型和损失函数
model = Network()
loss_fn = nn.BCEWithLogitsLoss()
将神经网络和损失函数的调用封装为一个前向计算函数以使用函数式微分
def forward_fn(x, y):
z = model(x)
loss = loss_fn(z, y)
return loss
再使用value_and_grad接口获取导函数用于计算梯度。
由于使用Cell封装神经网络模型,模型的参数为Cell内部属性,不再需要指定grad_position,配置为None,而使用weights参数及model.trainable_params()方法获取Cell中可以求导的参数。
grad_fn = mindspore.value_and_grad(forward_fn, None, weights = model.trainable_params())
loss, grads = grad_fn(x, y)
print(grads)