昇思25天学习打卡营第6天 | 函数式自动微分

1. 函数式自动微分

神经网络的训练主要使用反向传播算法,模型预测值(logits)与正确标签(label)送入损失函数(loss function)获得loss,然后进行反向传播计算,求得梯度(gradients),最终更新至模型参数(parameters)。自动微分能够计算可导函数在某点处的导数值,是反向传播算法的一般化。自动微分主要解决的问题是将一个复杂的数学运算分解为一系列简单的基本运算,该功能对用户屏蔽了大量的求导细节和过程,大大降低了框架的使用门槛。

恩,作为一个小白来说,我觉得有必要补充一下梯度的概念
在深度学习中,梯度(Gradient)是一个非常重要的概念,它表示函数在某一点上的局部变化率。在多变量函数的情况下,梯度是一个向量,它的每个分量都表示函数在相应维度上的变化率。
具体来说,对于一个多变量函数 f(x1, x2, …, xn),其梯度∇f(x) 是一个向量,其每个分量 ∂ x ∂ f \frac{∂x}{∂f} fx表示函数在 xi 方向上的变化率。梯度的大小表示函数在该点的局部斜率,而梯度的方向表示函数在该点上升最快的方向。
梯度的计算通常遵循以下步骤:
1、对每个变量求偏导数:对于函数 f(x),对每个变量 ∂ f ∂ x i \frac{∂f}{∂xi} xif
2、将这些偏导数组合成梯度向量:将所有偏导数组合起来,形成一个向量,这就是梯度向量。在多变量函数的情况下,梯度向量是所有偏导数的集合。
例如,对于一个简单的函数 f(x, y) = x2 + y2 ,其梯度向量是:∇f(x,y)=[ ∂ f ∂ x \frac{∂f}{∂x} xf, ∂ f ∂ x \frac{∂f}{∂x} xf]=[2x,2y]
在实际应用中,梯度的计算通常使用链式法则,这涉及到多个变量的偏导数,用于指导模型的参数更新。

MindSpore使用函数式自动微分的设计理念,提供更接近于数学语义的自动微分接口gradvalue_and_grad。下面我们使用一个简单的单层线性变换模型进行介绍。

1.1 导入依赖

# 导入numpy库,并重命名为np,以便在代码中方便地使用numpy的功能。
import numpy as np

# 导入mindspore库,这是华为推出的一个开源深度学习框架。
import mindspore

# 从mindspore库中导入nn模块,该模块提供了构建神经网络所需的层和函数。
from mindspore import nn

# 从mindspore库中导入ops模块,该模块提供了神经网络操作的各种运算符。
from mindspore import ops

# 从mindspore库中导入Tensor和Parameter类,这两个类分别用于创建张量对象和参数对象,它们是构建神经网络的基础。
from mindspore import Tensor, Parameter

1.2 函数与计算图

计算图是用图论语言表示数学函数的一种方式,也是深度学习框架表达神经网络模型的统一方法。我们将根据下面的计算图构造计算函数和神经网络。

compute-graph
在这个模型中, x x x为输入, y y y为正确值, w w w b b b是我们需要优化的参数。

# 创建输入张量
# 使用ops模块中的ones函数创建一个形状为(5,)的一维张量,所有元素都初始化为1,数据类型为float32。
x = ops.ones(5, mindspore.float32)  

# 创建预期输出张量
# 使用ops模块中的zeros函数创建一个形状为(3,)的一维张量,所有元素都初始化为0,数据类型为float32。
y = ops.zeros(3, mindspore.float32)  

# 创建一个参数对象w,其值为一个5x3的二维张量,元素值从标准正态分布中随机采样,数据类型为float32。
# 这个张量将作为神经网络中的权重(weights)。
w = Parameter(Tensor(np.random.randn(5, 3), mindspore.float32), name='w') 

# 创建一个参数对象b,其值为一个形状为(3,)的一维张量,元素值从标准正态分布中随机采样,数据类型为float32。
# 这个张量将作为神经网络中的偏置(biases)。
b = Parameter(Tensor(np.random.randn(3,), mindspore.float32), name='b') 

我们根据计算图描述的计算过程,构造计算函数。
其中,binary_cross_entropy_with_logits 是一个损失函数,计算预测值和目标值之间的二值交叉熵损失。

# 定义一个函数function,它接受四个参数:x(输入数据),y(标签),w(权重),b(偏置)。
def function(x, y, w, b):

    # 使用ops模块中的matmul函数计算x和w的矩阵乘法,然后加上偏置b。
    # 这个操作实现了线性变换,z是模型的预测值。
    z = ops.matmul(x, w) + b

    # 调用ops模块中的binary_cross_entropy_with_logits函数计算二进制交叉熵损失。
    # 这个损失函数适用于二分类问题,其中z是模型的预测值(在计算损失之前通常不需要经过激活函数)。
    # y是真实的标签值,它应该是一个与z相同形状的张量,包含0和1。
    # ops.ones_like(z)创建了一个与z形状相同的张量,所有元素都是1,用作损失函数的参数。
    # 这个函数的返回值是损失,一个标量值,表示模型预测值和真实值之间的差异。
    # 损失越小,表示模型的预测越接近真实标签。
    # 在训练过程中,我们通常使用梯度下降等优化算法来最小化这个损失函数,从而提高模型的性能。
    loss = ops.binary_cross_entropy_with_logits(z, y, ops.ones_like(z), ops.ones_like(z))

    # 返回计算得到的损失值。
    return loss
    
# 调用定义的function函数,传入x、y、w和b参数,计算二进制交叉熵损失。
loss = function(x, y, w, b)

# 使用print函数将计算得到的损失值输出。
print(loss)

输出:

0.17323041

1.3 微分函数与梯度计算

为了优化模型参数,需要求参数对loss的导数: ∂ loss ⁡ ∂ w \frac{\partial \operatorname{loss}}{\partial w} wloss ∂ loss ⁡ ∂ b \frac{\partial \operatorname{loss}}{\partial b} bloss,此时我们调用mindspore.grad函数,来获得function的微分函数。

这里使用了grad函数的两个入参,分别为:

  • fn:待求导的函数。
  • grad_position:指定求导输入位置的索引。

由于我们对 w w w b b b求导,因此配置其在function入参对应的位置(2, 3)

使用grad获得微分函数是一种函数变换,即输入为函数,输出也为函数。

# 使用mindspore.grad函数来创建一个梯度函数grad_fn,它将计算function函数关于其第三个和第四个参数(w和b)的梯度。
# 参数function是我们之前定义的函数,其计算二进制交叉熵损失。
# 参数(2, 3)是一个元组,表示我们想要计算的是function函数关于其参数列表中的第二和第三个参数的梯度。
# 因此,grad_fn将会返回一个函数,当我们调用这个返回的函数并传入相应的参数时,它将计算并返回function函数关于w和b的梯度。
grad_fn = mindspore.grad(function, (2, 3))

# 调用创建的梯度函数grad_fn,传入x、y、w和b参数,计算function函数关于w和b的梯度。
# grads将是一个包含两个元素的元组,第一个元素是关于w的梯度,第二个元素是关于b的梯度。
grads = grad_fn(x, y, w, b)

# 使用print函数将计算得到的梯度输出到控制台。
print(grads)

输出:

(Tensor(shape=[5, 3], dtype=Float32, value=
[[ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03]]), 
 Tensor(shape=[3], dtype=Float32, value= 
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03]))

输出的梯度包含两个Tensor对象,分别对应于权重 w 和偏置 b 的梯度。这些梯度表示在当前的参数值和给定的输入 x 和标签 y 下,损失函数对于每个参数的变化率。换句话说,它们指示了为了减少损失,每个参数应该移动的方向和幅度。
对于权重 w 的梯度,它是一个形状为 [5, 3] 的二维张量,这意味着你有5个输入和3个输出。每个元素 w[i, j] 的梯度表示如果稍微改变 w[i, j] 的值,损失函数的值会如何变化。正的梯度意味着增加 w[i, j] 的值可以减少损失,而负的梯度意味着减少 w[i, j] 的值可以减少损失
对于偏置 b 的梯度,它是一个形状为 [3] 的一维张量,每个元素 b[j] 的梯度表示如果稍微改变 b[j] 的值,损失函数的值会如何变化。同样,正的梯度意味着增加 b[j] 的值可以减少损失,而负的梯度意味着减少 b[j] 的值可以减少损失。
此处,所有的梯度都是正的,这意味着增加权重和偏置的值可以减少损失函数的值。然而,需要注意的是,这些梯度只是在这个特定点的局部信息,它们指示了在这个点附近如何优化参数。在实际的优化过程中,你需要结合学习率来更新参数,以在多次迭代中逐步减少损失。
此外,权重 w 的梯度矩阵中的所有行都是相同的,这可能表明输入 x 是相同的,或者模型在当前状态下对于所有输入有相同的预测。如果输入 x 是不同的,这可能是模型结构的一个特点,比如在批处理中所有输入都被视为相同的情况。

1.4 Stop Gradient(阻止梯度传播)

在深度学习框架中,Stop Gradient(在某些框架中可能被称为 detach 或其他类似名称)是一个操作,用于阻止梯度在计算图中的传播。当你对某个张量应用 Stop Gradient 操作后,该张量的梯度将不会在反向传播过程中被计算,即该张量在计算图中的梯度将被视为0。
这个操作的目的是在某些情况下冻结模型的一部分,使其在训练过程中不被更新。这可以在以下几种情况下使用:

  • 特征提取:如果你想在预训练的模型上添加自定义层,并只训练这些新添加的层,你可以将预训练层的输出与 Stop Gradient 结合使用,这样预训练层的参数就不会在训练过程中被更新。
  • 固定某些参数:在某些模型架构中,你可能想要固定某些参数,只训练其他参数。通过在这些参数上应用 Stop Gradient,可以确保它们在训练过程中保持不变。
  • 调试和实验:在调试模型或进行实验时,你可能想要检查模型中某一部分的输出对损失的影响,而不想影响其他部分的训练。在这种情况下,可以在特定部分应用 Stop Gradient 来隔离其影响。
  • 避免梯度消失或爆炸:在某些情况下,应用 Stop Gradient 可以帮助防止梯度消失或爆炸问题,特别是在非常深的网络中。
    在MindSpore中,可以使用 ops.stop_gradient 函数来实现这个操作。

通常情况下,求导时会求loss对参数的导数,因此函数的输出只有loss一项。当我们希望函数输出多项时,微分函数会求所有输出项对参数的导数。此时如果想实现对某个输出项的梯度截断,或消除某个Tensor对梯度的影响,需要用到Stop Gradient操作。

这里我们将function改为同时输出loss和z的function_with_logits,获得微分函数并执行。

# 定义一个函数function_with_logits,它接受四个参数:x(输入数据),y(标签),w(权重),b(偏置)。
def function_with_logits(x, y, w, b):

    # 使用ops模块中的matmul函数计算x和w的矩阵乘法,然后加上偏置b。
    # 这个操作实现了线性变换,z是模型的预测值(logits)。
    z = ops.matmul(x, w) + b

    # 调用ops模块中的binary_cross_entropy_with_logits函数计算二进制交叉熵损失。
    # 这个损失函数适用于二分类问题,其中z是模型的预测值(在计算损失之前通常不需要经过激活函数)。
    # y是真实的标签值,它应该是一个与z相同形状的张量,包含0和1。
    # ops.ones_like(z)创建了一个与z形状相同的张量,所有元素都是1,用作损失函数的参数。
    # 这个函数的返回值是损失,一个标量值,表示模型预测值和真实值之间的差异。
    loss = ops.binary_cross_entropy_with_logits(z, y, ops.ones_like(z), ops.ones_like(z))

    # 返回计算得到的损失值和logits。
    return loss, z

# 使用mindspore.grad函数来创建一个梯度函数grad_fn,它将计算function_with_logits函数关于其第二个和第三个参数(w和b)的梯度。
# 参数function_with_logits是我们之前定义的函数,它返回损失和logits。
# 参数(2, 3)是一个元组,表示我们想要计算的是function_with_logits函数关于其参数列表中的第三和第四个参数的梯度。
grad_fn = mindspore.grad(function_with_logits, (2, 3))

# 调用之前创建的梯度函数grad_fn,传入x、y、w和b参数,计算function_with_logits函数关于w和b的梯度。
# grads将是一个包含两个元素的元组,第一个元素是关于w的梯度,第二个元素是关于b的梯度。
grads = grad_fn(x, y, w, b)

# 使用print函数将计算得到的梯度输出到控制台。
print(grads)

输出:

(Tensor(shape=[5, 3], dtype=Float32, value=
[[ 1.05551052e+00,  1.08844924e+00,  1.00958407e+00],
 [ 1.05551052e+00,  1.08844924e+00,  1.00958407e+00],
 [ 1.05551052e+00,  1.08844924e+00,  1.00958407e+00],
 [ 1.05551052e+00,  1.08844924e+00,  1.00958407e+00],
 [ 1.05551052e+00,  1.08844924e+00,  1.00958407e+00]]), 
 Tensor(shape=[3], dtype=Float32, value= 
 [ 1.05551052e+00,  1.08844924e+00,  1.00958407e+00]))

可以看到求得 𝑤、𝑏对应的梯度值发生了变化。此时如果想要屏蔽掉z对梯度的影响,即仍只求参数对loss的导数,可以使用ops.stop_gradient接口,将梯度在此处截断。我们将function实现加入stop_gradient,并执行。

# 定义一个函数function_stop_gradient,它接受四个参数:x(输入数据),y(标签),w(权重),b(偏置)。
def function_stop_gradient(x, y, w, b):

    # 使用ops模块中的matmul函数计算x和w的矩阵乘法,然后加上偏置b。
    # 这个操作实现了线性变换,z是模型的预测值(logits)。
    z = ops.matmul(x, w) + b

    # 调用ops模块中的binary_cross_entropy_with_logits函数计算二进制交叉熵损失。
    # 这个损失函数适用于二分类问题,其中z是模型的预测值(在计算损失之前通常不需要经过激活函数)。
    # y是真实的标签值,它应该是一个与z相同形状的张量,包含0和1。
    # ops.ones_like(z)创建了一个与z形状相同的张量,所有元素都是1,用作损失函数的参数。
    # 这个函数的返回值是损失,一个标量值,表示模型预测值和真实值之间的差异。
    loss = ops.binary_cross_entropy_with_logits(z, y, ops.ones_like(z), ops.ones_like(z))

    # 使用ops.stop_gradient函数创建一个停止梯度传播的z副本。
    # 这意味着在计算梯度时,z的梯度将被视为0,不会影响其他变量的梯度计算。
    # 返回计算得到的损失值和停止梯度传播的logits。
    return loss, ops.stop_gradient(z)

# 使用mindspore.grad函数来创建一个梯度函数grad_fn,
# 它将计算function_stop_gradient函数关于其第三个和第四个参数(w和b)的梯度。
# 参数function_stop_gradient是我们之前定义的函数,它返回损失和经过stop_gradient处理的logits。
# 参数(2, 3)是一个元组,表示我们想要计算的是function_stop_gradient函数关于其参数列表中的第三和第四个参数的梯度。
grad_fn = mindspore.grad(function_stop_gradient, (2, 3))

# 调用之前创建的梯度函数grad_fn,传入x、y、w和b参数,计算function_stop_gradient函数关于w和b的梯度。
# grads将是一个包含两个元素的元组,第一个元素是关于w的梯度,第二个元素是关于b的梯度。
grads = grad_fn(x, y, w, b)

# 使用print函数将计算得到的梯度输出到控制台。
print(grads)

输出:

(Tensor(shape=[5, 3], dtype=Float32, value=
[[ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03]]), 
 Tensor(shape=[3], dtype=Float32, value= 
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03]))

可以看到,求得 w w w b b b对应的梯度值与初始function求得的梯度值一致。

1.5 Auxiliary data(辅助数据)

Auxiliary data意为辅助数据,是函数除第一个输出项外的其他输出。通常我们会将函数的loss设置为函数的第一个输出,其他的输出即为辅助数据。

gradvalue_and_grad提供has_aux参数,当其设置为True时,可以自动实现前文手动添加stop_gradient的功能,满足返回辅助数据的同时不影响梯度计算的效果。

下面仍使用function_with_logits,配置has_aux=True,并执行。

# 使用mindspore.grad函数来创建一个梯度函数grad_fn,
# 它将计算function_with_logits函数关于其第三个和第四个参数(w和b)的梯度。
# 参数function_with_logits是我们之前定义的函数,它返回损失和logits。
# 参数(2, 3)是一个元组,表示我们想要计算的是function_with_logits函数关于其参数列表中的第三和第四个参数的梯度。
# 参数has_aux=True指示函数的返回值是一个元组,其中第一个元素是需要计算梯度的主要输出(需要计算梯度),
# 而其余元素是辅助输出(不需要计算梯度)。
grad_fn = mindspore.grad(function_with_logits, (2, 3), has_aux=True)

# 调用之前创建的梯度函数grad_fn,传入x、y、w和b参数,计算function_with_logits函数关于w和b的梯度。
# 由于has_aux=True,grad_fn的返回值将是一个元组,其中第一个元素是关于w和b的梯度,第二个元素是辅助输出,即logits。
grads, (z,) = grad_fn(x, y, w, b)

# 使用print函数将计算得到的梯度输出到控制台。
# grads是关于w和b的梯度,z是logits。
print(grads, z)

输出:

(Tensor(shape=[5, 3], dtype=Float32, value=
[[ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03],
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03]]), 
 Tensor(shape=[3], dtype=Float32, value= 
 [ 5.55104874e-02,  8.84492472e-02,  9.58411861e-03])) 
 [-1.6104116 -1.0183561 -3.5198617]

可以看到,求得 w w w b b b对应的梯度值与初始function求得的梯度值一致,同时z能够作为微分函数的输出返回。

1.6 神经网络梯度计算

前述章节主要根据计算图对应的函数介绍了MindSpore的函数式自动微分,但我们的神经网络构造是继承自面向对象编程范式的nn.Cell。接下来我们通过Cell构造同样的神经网络,利用函数式自动微分来实现反向传播。
首先我们继承nn.Cell构造单层线性变换神经网络。这里我们直接使用前文的 w w w b b b作为模型参数,使用mindspore.Parameter进行包装后,作为内部属性,并在construct内实现相同的Tensor操作。

# 定义一个名为Network的神经网络类,它继承自nn.Cell。
# nn.Cell是MindSpore中用于构建神经网络的基本单元。
class Network(nn.Cell):
    def __init__(self):
    	# 调用父类的构造函数,确保正确的初始化。
        super().__init__()  
        # 将外部定义的权重w赋值给网络的属性。
        self.w = w  
        # 将外部定义的偏置b赋值给网络的属性。
        self.b = b  

    # 定义construct方法,这是MindSpore中实现前向传播的函数。
    def construct(self, x):
    	# 使用ops.matmul计算x和w的矩阵乘法,然后加上偏置b。
        z = ops.matmul(x, self.w) + self.b  
        return z  # 返回计算得到的线性变换结果z。

# 创建Network类的实例,用于构建和训练模型。
model = Network()

# 创建一个二进制交叉熵损失函数,它适用于二分类问题,其中模型的输出是经过sigmoid激活函数处理后的值。
# 这里的loss_fn将用于计算模型的预测值和真实标签之间的差异。
loss_fn = nn.BCEWithLogitsLoss()

# 封装一个名为forward_fn的函数,用于计算模型的前向传播和损失。
# 参数x是输入数据,参数y是真实标签。
def forward_fn(x, y):
	# 使用模型计算输入x的预测值z。
    z = model(x)  
    # 使用损失函数计算预测值z和真实标签y之间的差异。
    loss = loss_fn(z, y) 
    # 返回计算得到的损失值。
    return loss  

# 使用mindspore.value_and_grad函数来创建一个梯度函数grad_fn,
# 它将计算forward_fn函数关于模型参数的梯度。
# 参数forward_fn是我们之前定义的函数,它计算模型的前向传播和损失。
# 参数None表示不需要任何额外的输入。
# 参数weights=model.trainable_params()指定了需要计算梯度的参数。
# model.trainable_params()返回模型中所有可训练的参数的列表。
grad_fn = mindspore.value_and_grad(forward_fn, None, weights=model.trainable_params())

# 调用之前创建的梯度函数grad_fn,传入x和y参数,计算forward_fn函数关于模型参数的梯度。
# grad_fn的返回值是一个元组,其中第一个元素是损失值,第二个元素是关于模型参数的梯度。
loss, grads = grad_fn(x, y)

# 使用print函数将计算得到的损失值和梯度输出到控制台。
print(grads)

输了:
梯度

可以看到梯度值和前文function求得的梯度值一致。

2. 小结

本文主要介绍了mindspore函数式自动微分的实现方法。
通过使用一个简单的单层线性变换模型实例,

  • 介绍了深度学习框架表达神经网络模型的统一方法——计算图,
  • 根据计算图完成计算函数的构建,
  • 使用自动微分方法计算函数的梯度,
  • 介绍了阻止梯度传播的方法,
  • 介绍了辅助数据的用法
  • 通过函数式自动微分实现神经网络的反向传播
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值