在线运行下载Notebook下载样例代码查看源文件

Cell与参数

Cell作为神经网络构造的基础单元,与神经网络层(Layer)的概念相对应,对Tensor计算操作的抽象封装,能够更准确清晰地对神经网络结构进行表示。除了基础的Tensor计算流程定义外,神经网络层还包含了参数管理、状态管理等功能。而参数(Parameter)是神经网络训练的核心,通常作为神经网络层的内部成员变量。本节我们将系统介绍参数、神经网络层以及其相关使用方法。

Parameter

参数(Parameter)是一类特殊的Tensor,是指在模型训练过程中可以对其值进行更新的变量。MindSpore提供mindspore.Parameter类进行Parameter的构造。为了对不同用途的Parameter进行区分,下面对两种不同类别的Parameter进行定义:

  • 可训练参数。在模型训练过程中根据反向传播算法求得梯度后进行更新的Tensor,此时需要将required_grad设置为True

  • 不可训练参数。不参与反向传播,但需要更新值的Tensor(如BatchNorm中的meanvar变量),此时需要将requires_grad设置为False

Parameter默认设置required_grad=True

下面我们构造一个简单的全连接层:

[1]:
import numpy as np
import mindspore
from mindspore import nn
from mindspore import ops
from mindspore import Tensor, Parameter

class Network(nn.Cell):
    def __init__(self):
        super().__init__()
        self.w = Parameter(Tensor(np.random.randn(5, 3), mindspore.float32), name='w') # weight
        self.b = Parameter(Tensor(np.random.randn(3,), mindspore.float32), name='b') # bias

    def construct(self, x):
        z = ops.matmul(x, self.w) + self.b
        return z

net = Network()

Cell__init__方法中,我们定义了wb两个Parameter,并配置name进行命名空间管理。在construct方法中使用self.attr直接调用参与Tensor运算。

获取Parameter

在使用Cell+Parameter构造神经网络层后,我们可以使用多种方法来获取Cell管理的Parameter。

获取单个参数

单独获取某个特定参数,直接调用Python类的成员变量即可。

[2]:
print(net.b.asnumpy())
[-1.2192779  -0.36789745  0.0946381 ]

获取可训练参数

可使用Cell.trainable_params方法获取可训练参数,通常在配置优化器时需调用此接口。

[3]:
print(net.trainable_params())
[Parameter (name=w, shape=(5, 3), dtype=Float32, requires_grad=True), Parameter (name=b, shape=(3,), dtype=Float32, requires_grad=True)]

获取所有参数

使用Cell.get_parameters()方法可获取所有参数,此时会返回一个Python迭代器。

[4]:
print(type(net.get_parameters()))
<class 'generator'>

或者可以调用Cell.parameters_and_names返回参数名称及参数。

[5]:
for name, param in net.parameters_and_names():
    print(f"{name}:\n{param.asnumpy()}")
w:
[[ 4.15680408e-02 -1.20311625e-01  5.02573885e-02]
 [ 1.22175144e-04 -1.34980649e-01  1.17642188e+00]
 [ 7.57667869e-02 -1.74758151e-01 -5.19092619e-01]
 [-1.67846107e+00  3.27240258e-01 -2.06452996e-01]
 [ 5.72323874e-02 -8.27963874e-02  5.94243526e-01]]
b:
[-1.2192779  -0.36789745  0.0946381 ]

修改Parameter

直接修改参数值

Parameter是一种特殊的Tensor,因此可以使用Tensor索引修改的方式对其值进行修改。

[6]:
net.b[0] = 1.
print(net.b.asnumpy())
[ 1.         -0.36789745  0.0946381 ]

覆盖修改参数值

可调用Parameter.set_data方法,使用相同Shape的Tensor对Parameter进行覆盖。该方法常用于使用Initializer进行Cell遍历初始化

[7]:
net.b.set_data(Tensor([3, 4, 5]))
print(net.b.asnumpy())
[3. 4. 5.]

运行时修改参数值

参数的主要作用为模型训练时对其值进行更新,在反向传播获得梯度后,或不可训练参数需要进行更新,都涉及到运行时参数修改。由于MindSpore的计算图编译设计,此时需要使用mindspore.ops.assign接口对参数进行赋值。该方法常用于自定义优化器场景。下面是一个简单的运行时修改参数值样例:

[8]:
import mindspore as ms

@ms.jit
def modify_parameter():
    b_hat = ms.Tensor([7, 8, 9])
    ops.assign(net.b, b_hat)
    return True

modify_parameter()
print(net.b.asnumpy())
[7. 8. 9.]

Parameter Tuple

变量元组ParameterTuple,用于保存多个Parameter,继承于元组tuple,提供克隆功能。

如下示例提供ParameterTuple创建方法:

[10]:
from mindspore.common.initializer import initializer
from mindspore import ParameterTuple
# 创建
x = Parameter(default_input=ms.Tensor(np.arange(2 * 3).reshape((2, 3))), name="x")
y = Parameter(default_input=initializer('ones', [1, 2, 3], ms.float32), name='y')
z = Parameter(default_input=2.0, name='z')
params = ParameterTuple((x, y, z))

# 从params克隆并修改名称为"params_copy"
params_copy = params.clone("params_copy")

print(params)
print(params_copy)
(Parameter (name=x, shape=(2, 3), dtype=Int64, requires_grad=True), Parameter (name=y, shape=(1, 2, 3), dtype=Float32, requires_grad=True), Parameter (name=z, shape=(), dtype=Float32, requires_grad=True))
(Parameter (name=params_copy.x, shape=(2, 3), dtype=Int64, requires_grad=True), Parameter (name=params_copy.y, shape=(1, 2, 3), dtype=Float32, requires_grad=True), Parameter (name=params_copy.z, shape=(), dtype=Float32, requires_grad=True))

Cell训练状态转换

神经网络中的部分Tensor操作在训练和推理时的表现并不相同,如nn.Dropout在训练时进行随机丢弃,但在推理时则不丢弃,nn.BatchNorm在训练时需要更新meanvar两个变量,在推理时则固定其值不变。因此我们可以通过Cell.set_train接口来设置神经网络的状态。

set_train(True)时,神经网络状态为train, set_train接口默认值为True:

[11]:
net.set_train()
print(net.phase)
train

set_train(False)时,神经网络状态为predict

[12]:
net.set_train(False)
print(net.phase)
predict

自定义神经网络层

通常情况下,MindSpore提供的神经网络层接口和function函数接口能够满足模型构造需求,但由于AI领域不断推陈出新,因此有可能遇到新网络结构没有内置模块的情况。此时我们可以根据需要,通过MindSpore提供的function接口、Primitive算子自定义神经网络层,并可以使用Cell.bprop方法自定义反向。下面分别详述三种自定义方法。

使用function接口构造神经网络层

MindSpore提供大量基础的function接口,可以使用其构造复杂的Tensor操作,封装为神经网络层。下面以Threshold为例,其公式如下:

\[\begin{split}y =\begin{cases} x, &\text{ if } x > \text{threshold} \\ \text{value}, &\text{ otherwise } \end{cases}\end{split}\]

可以看到Threshold判断Tensor的值是否大于threshold值,保留判断结果为True的值,替换判断结果为False的值。因此,对应实现如下:

[43]:
class Threshold(nn.Cell):
    def __init__(self, threshold, value):
        super().__init__()
        self.threshold = threshold
        self.value = value

    def construct(self, inputs):
        cond = ops.gt(inputs, self.threshold)
        value = ops.fill(inputs.dtype, inputs.shape, self.value)
        return ops.select(cond, inputs, value)

这里分别使用了ops.gtops.fillops.select来实现判断和替换。下面执行自定义的Threshold层:

[45]:
m = Threshold(0.1, 20)
inputs = mindspore.Tensor([0.1, 0.2, 0.3], mindspore.float32)
m(inputs)
[45]:
Tensor(shape=[3], dtype=Float32, value= [ 2.00000000e+01,  2.00000003e-01,  3.00000012e-01])

可以看到inputs[0] = threshold, 因此被替换为20

自定义Cell反向

在特殊场景下,我们不但需要自定义神经网络层的正向逻辑,也需要手动控制其反向的计算,此时我们可以通过Cell.bprop接口对其反向进行定义。在全新的神经网络结构设计、反向传播速度优化等场景下会用到该功能。下面我们以Dropout2d为例,介绍如何自定义Cell反向:

[55]:
class Dropout2d(nn.Cell):
    def __init__(self, keep_prob):
        super().__init__()
        self.keep_prob = keep_prob
        self.dropout2d = ops.Dropout2D(keep_prob)

    def construct(self, x):
        return self.dropout2d(x)

    def bprop(self, x, out, dout):
        _, mask = out
        dy, _ = dout
        if self.keep_prob != 0:
            dy = dy * (1 / self.keep_prob)
        dy = mask.astype(mindspore.float32) * dy
        return (dy.astype(x.dtype), )

dropout_2d = Dropout2d(0.8)
dropout_2d.bprop_debug = True

bprop方法分别有三个入参:

  • x: 正向输入,当正向输入为多个时,需同样数量的入参。

  • out: 正向输出。

  • dout: 反向传播时,当前Cell执行之前的反向结果。

一般我们需要根据正向输出和前层反向结果配合,根据反向求导公式计算反向结果,并将其返回。Dropout2d的反向计算需要根据正向输出的mask矩阵对前层反向结果进行mask,然后根据keep_prob进行缩放。最终可得到正确的计算结果。