神经网络的训练主要使用反向传播算法,模型预测值(logits)与正确标签(label)送入损失函数(loss function)获得loss,然后进行反向传播计算,求得梯度(gradients),最终更新至模型参数(parameters)。自动微分能够计算可导函数在某点处的导数值,是反向传播算法的一般化。自动微分主要解决的问题是将一个复杂的数学运算分解为一系列简单的基本运算,该功能对用户屏蔽了大量的求导细节和过程,大大降低了框架的使用门槛。
MindSpore使用函数式自动微分的设计理念,提供更接近于数学语义的自动微分接口grad
和value_and_grad
。下面我们使用一个简单的单层线性变换模型进行介绍。
import numpy as np
import mindspore
from mindspore import nn
from mindspore import ops
from mindspore import Tensor, Parameter
1、函数与计算图
计算图是用图论语言表示数学函数的一种方式,也是深度学习框架表达神经网络模型的统一方法。我们将根据下面的计算图构造计算函数和神经网络。
在这个模型中,𝑥为输入,𝑦为正确值,𝑤和𝑏是我们需要优化的参数。
x = ops.ones(5, mindspore.float32) # input tensor
y = ops.zeros(3, mindspore.float32) # expected output
w = Parameter(Tensor(np.random.randn(5, 3), mindspore.float32), name='w') # weight
b = Parameter(Tensor(np.random.randn(3,), mindspore.float32), name='b') # bias
print(x)
print(y)
2、构造损失函数
我们根据计算图描述的计算过程,构造计算函数。 其中,binary_cross_entropy_with_logits 是一个损失函数,计算预测值和目标值之间的二值交叉熵损失。
mindspore.ops.binary_cross_entropy_with_logits(logits, label, weight=None, pos_weight=None, reduction='mean')
输入经过sigmoid激活函数后作为预测值,binary_cross_entropy_with_logits 计算预测值和目标值之间的二值交叉熵损失。
将输入 logits 设置为 𝑋 ,输入 labels 设置为 𝑌 ,输入 weight 设置为 𝑊 ,输出设置为 𝐿 。则,
𝑖 表示 𝑖^𝑡ℎ 样例, 𝑗 表示类别。则,
ℓ 表示计算损失的方法。
有三种方法:第一种方法是直接提供损失值,第二种方法是计算所有损失的平均值,第三种方法是计算所有损失的总和。
该算子会将输出乘以相应的权重。 𝑤𝑒𝑖𝑔ℎ𝑡 表示一个batch中的每条数据分配不同的权重, 𝑝𝑜𝑠_𝑤𝑒𝑖𝑔ℎ𝑡 为每个类别的正例子添加相应的权重。
此外,它可以通过向正例添加权重来权衡召回率和精度。 在多标签分类的情况下,损失可以描述为:
其中 c 是类别数目(c>1 表示多标签二元分类,c=1 表示单标签二元分类),n 是批次中样本的数量,𝑃𝑐 是 第c类正例的权重。 𝑃𝑐>1 增大召回率, 𝑃𝑐<1 增大精度。
【参数】
-
logits (Tensor) - 输入预测值。其数据类型为float16或float32。
-
label (Tensor) - 输入目标值,shape与 logits 相同。数据类型为float16或float32。
-
weight (Tensor,可选) - 指定每个批次二值交叉熵的权重。支持广播,使其shape与 logits 的shape保持一致。数据类型必须为float16或float32。默认值:
None
, weight 是值为1
的Tensor。 -
pos_weight (Tensor,可选) - 指定正类的权重。是一个长度等于分类数的向量。支持广播,使其shape与 logits 的shape保持一致。数据类型必须为float16或float32。默认值:
None
, pos_weight 是值为1
的Tensor。 -
reduction (str,可选) - 指定应用于输出结果的规约计算方式,可选
'none'
、'mean'
、'sum'
,默认值:'mean'
。-
'none'
:不应用规约方法。 -
'mean'
:计算输出元素的加权平均值。 -
'sum'
:计算输出元素的总和。
-
def function(x, y, w, b):
z = ops.matmul(x, w) + b
#mindspore.ops.matmul(input, other)计算两个数组的乘积。
#input (Tensor) - 输入Tensor,不支持Scalar, input 的最后一维度和 other 的倒数第二维度相
#等,且 input 和 other 彼此支持广播。
#other (Tensor) - 输入Tensor,不支持Scalar, input 的最后一维度和 other 的倒数第二维度相
#等,且 input 和 other 彼此支持广播。
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)
3、微分函数与梯度计算
为了优化模型参数,需要求参数对loss的导数:∂loss/∂𝑤和∂loss/∂𝑏,此时我们调用mindspore.grad
函数,来获得function
的微分函数。
mindspore.grad(fn, grad_position=0, weights=None, has_aux=False, return_ids=False)
生成求导函数,用于计算给定函数的梯度。
函数求导包含以下三种场景:
-
对输入求导,此时 grad_position 非None,而 weights 是None;
-
对网络变量求导,此时 grad_position 是None,而 weights 非None;
-
同时对输入和网络变量求导,此时 grad_position 和 weights 都非None。
【参数】
-
fn (Union[Cell, Function]) - 待求导的函数或网络。
-
grad_position (Union[NoneType, int, tuple[int]]) - 指定求导输入位置的索引。若为int类型,表示对单个输入求导;若为tuple类型,表示对tuple内索引的位置求导,其中索引从0开始;若是None,表示不对输入求导,这种场景下, weights 非None。默认值:
0
。 -
weights (Union[ParameterTuple, Parameter, list[Parameter]]) - 训练网络中需要返回梯度的网络变量。一般可通过 weights = net.trainable_params() 获取。默认值:
None
。 -
has_aux (bool) - 是否返回辅助参数的标志。若为
True
, fn 输出数量必须超过一个,其中只有 fn 第一个输出参与求导,其他输出值将直接返回。默认值:False
。 -
return_ids (bool) - 是否返回由返回的梯度和指定求导输入位置的索引或网络变量组成的tuple。若为
True
,其输出中所有的梯度值将被替换为:由该梯度和其输入的位置索引,或者用于计算该梯度的网络变量组成的tuple。默认值:False
。
这里使用了grad
函数的两个入参,分别为:
fn
:待求导的函数。grad_position
:指定求导输入位置的索引。
由于我们对𝑤和𝑏求导,因此配置其在function
入参对应的位置(2, 3)
。
使用grad
获得微分函数是一种函数变换,即输入为函数,输出也为函数。
grad_fn = mindspore.grad(function, (2, 3))
# 执行微分函数,即可获得 𝑤、 𝑏对应的梯度。
grads = grad_fn(x, y, w, b)
print(grads)
3.1 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)
可以看到求得𝑤𝑤、𝑏𝑏对应的梯度值发生了变化。此时如果想要屏蔽掉z对梯度的影响,即仍只求参数对loss的导数,可以使用ops.stop_gradient
接口,将梯度在此处截断。我们将function
实现加入stop_gradient
,并执行。
mindspore.ops.stop_gradient(value)
用于消除某个值对梯度的影响,例如截断来自于函数输出的梯度传播。
【参数】
-
value (Any) - 需要被消除梯度影响的值。
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)
可以看到,求得𝑤𝑤、𝑏𝑏对应的梯度值与初始function
求得的梯度值一致。
3.2 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)
可以看到,求得𝑤𝑤、𝑏𝑏对应的梯度值与初始function
求得的梯度值一致,同时z能够作为微分函数的输出返回。
4、神经网络梯度计算
神经网络构造是继承自面向对象编程范式的nn.Cell
。接下来我们通过Cell
构造同样的神经网络,利用函数式自动微分来实现反向传播。
首先我们继承nn.Cell
构造单层线性变换神经网络。这里我们直接使用前文的𝑤、𝑏作为模型参数,使用mindspore.Parameter
进行包装后,作为内部属性,并在construct
内实现相同的Tensor操作。
# Define model
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
接下来我们实例化模型和损失函数。
# Instantiate model
model = Network()
# Instantiate loss function
loss_fn = nn.BCEWithLogitsLoss()
完成后,由于需要使用函数式自动微分,需要将神经网络和损失函数的调用封装为一个前向计算函数。
# Define forward function
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)
执行微分函数,可以看到梯度值和前文function
求得的梯度值一致。