6.2. 参数管理
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

一旦我们选择了模型架构并设置了超参数,我们就可以进入训练循环,目标是找到最小化损失函数的参数值。训练完成后,我们将需要这些参数来进行未来的预测。此外,有时我们希望提取参数,以便在其他环境中重用它们,或者将模型保存到磁盘以便在其他软件中执行,或者为了获得科学上的理解而进行检查。

在大多数情况下,我们可以忽略参数声明和操作的细节,依赖深度学习框架来完成繁重的工作。但是,当我们开始使用带有标准层的堆叠式架构以外的模型时,我们有时需要深入研究参数的声明和操作。在本节中,我们将介绍以下内容:

  • 访问参数以进行调试、诊断和可视化。

  • 在不同模型组件之间共享参数。

import torch
from torch import nn
from mxnet import init, np, npx
from mxnet.gluon import nn

npx.set_np()
import jax
from flax import linen as nn
from jax import numpy as jnp
from d2l import jax as d2l
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
import tensorflow as tf

我们首先重点介绍一个具有单隐藏层的多层感知机。

net = nn.Sequential(nn.LazyLinear(8),
                    nn.ReLU(),
                    nn.LazyLinear(1))

X = torch.rand(size=(2, 4))
net(X).shape
torch.Size([2, 1])
net = nn.Sequential()
net.add(nn.Dense(8, activation='relu'))
net.add(nn.Dense(1))
net.initialize()  # Use the default initialization method

X = np.random.uniform(size=(2, 4))
net(X).shape
[21:49:32] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
(2, 1)
net = nn.Sequential([nn.Dense(8), nn.relu, nn.Dense(1)])

X = jax.random.uniform(d2l.get_key(), (2, 4))
params = net.init(d2l.get_key(), X)
net.apply(params, X).shape
(2, 1)
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(4, activation=tf.nn.relu),
    tf.keras.layers.Dense(1),
])

X = tf.random.uniform((2, 4))
net(X).shape
TensorShape([2, 1])

6.2.1. 参数访问

让我们先来看看如何从你已经知道的模型中访问参数。

当一个模型通过 Sequential 类定义时,我们可以像访问列表一样通过索引来访问模型的任何层。每一层的参数都方便地存储在其属性中。

当一个模型通过 Sequential 类定义时,我们可以像访问列表一样通过索引来访问模型的任何层。每一层的参数都方便地存储在其属性中。

正如您在之前定义的模型中可能观察到的,Flax和JAX将模型和参数解耦。当模型通过 Sequential 类定义时,我们首先需要初始化网络以生成参数字典。我们可以通过这个字典的键来访问任何层的参数。

当一个模型通过 Sequential 类定义时,我们可以像访问列表一样通过索引来访问模型的任何层。每一层的参数都方便地存储在其属性中。

我们可以如下检查第二个全连接层的参数。

net[2].state_dict()
OrderedDict([('weight',
              tensor([[-0.1649,  0.0605,  0.1694, -0.2524,  0.3526, -0.3414, -0.2322,  0.0822]])),
             ('bias', tensor([0.0709]))])
net[1].params
dense1_ (
  Parameter dense1_weight (shape=(1, 8), dtype=float32)
  Parameter dense1_bias (shape=(1,), dtype=float32)
)
params['params']['layers_2']
FrozenDict({
    kernel: Array([[ 0.2758769 ],
           [ 0.45259333],
           [ 0.28696904],
           [ 0.24622999],
           [-0.29272735],
           [ 0.07597765],
           [ 0.14919828],
           [ 0.18445292]], dtype=float32),
    bias: Array([0.], dtype=float32),
})
net.layers[2].weights
[<tf.Variable 'dense_1/kernel:0' shape=(4, 1) dtype=float32, numpy=
 array([[-0.892862  ],
        [ 0.7337135 ],
        [-0.05061114],
        [-0.97688395]], dtype=float32)>,
 <tf.Variable 'dense_1/bias:0' shape=(1,) dtype=float32, numpy=array([0.], dtype=float32)>]

我们可以看到,这个全连接层包含两个参数,分别对应于该层的权重和偏置。

6.2.1.1. 目标参数

请注意,每个参数都表示为参数类的一个实例。要对参数进行任何有用的操作,我们首先需要访问底层的数值。有几种方法可以做到这一点。有些方法更简单,而另一些方法则更通用。下面的代码从第二个神经网络层提取偏置,它返回一个参数类实例,并进一步访问该参数的值。

type(net[2].bias), net[2].bias.data
(torch.nn.parameter.Parameter, tensor([0.0709]))

参数是复杂的对象,包含值、梯度和附加信息。这就是为什么我们需要显式地请求值。

除了值之外,每个参数还允许我们访问梯度。因为我们还没有对这个网络调用反向传播,所以它处于初始状态。

net[2].weight.grad == None
True
type(net[1].bias), net[1].bias.data()
(mxnet.gluon.parameter.Parameter, array([0.]))

参数是复杂的对象,包含值、梯度和附加信息。这就是为什么我们需要显式地请求值。

除了值之外,每个参数还允许我们访问梯度。因为我们还没有对这个网络调用反向传播,所以它处于初始状态。

net[1].weight.grad()
array([[0., 0., 0., 0., 0., 0., 0., 0.]])
bias = params['params']['layers_2']['bias']
type(bias), bias
(jaxlib.xla_extension.ArrayImpl, Array([0.], dtype=float32))

与其他框架不同,JAX不跟踪神经网络参数的梯度,而是将参数和网络解耦。它允许用户将其计算表示为一个Python函数,并使用 grad 变换来实现相同目的。

type(net.layers[2].weights[1]), tf.convert_to_tensor(net.layers[2].weights[1])
(tensorflow.python.ops.resource_variable_ops.ResourceVariable,
 <tf.Tensor: shape=(1,), dtype=float32, numpy=array([0.], dtype=float32)>)

6.2.1.2. 一次性访问所有参数

当我们需要对所有参数执行操作时,逐个访问它们可能会变得繁琐。当我们处理更复杂的模型,例如嵌套模块时,情况可能会变得特别棘手,因为我们需要递归遍历整个树来提取每个子模块的参数。下面我们演示如何访问所有层的参数。

[(name, param.shape) for name, param in net.named_parameters()]
[('0.weight', torch.Size([8, 4])),
 ('0.bias', torch.Size([8])),
 ('2.weight', torch.Size([1, 8])),
 ('2.bias', torch.Size([1]))]
net.collect_params()
sequential0_ (
  Parameter dense0_weight (shape=(8, 4), dtype=float32)
  Parameter dense0_bias (shape=(8,), dtype=float32)
  Parameter dense1_weight (shape=(1, 8), dtype=float32)
  Parameter dense1_bias (shape=(1,), dtype=float32)
)
jax.tree_util.tree_map(lambda x: x.shape, params)
FrozenDict({
    params: {
        layers_0: {
            bias: (8,),
            kernel: (4, 8),
        },
        layers_2: {
            bias: (1,),
            kernel: (8, 1),
        },
    },
})
net.get_weights()
[array([[-0.06085569, -0.8411268 , -0.28591037,  0.31637532],
        [ 0.8330259 ,  0.4529298 ,  0.14709991, -0.18423098],
        [ 0.835087  ,  0.23927861, -0.7909084 , -0.49229068],
        [ 0.76430553,  0.40979892,  0.09074789,  0.4237972 ]],
       dtype=float32),
 array([0., 0., 0., 0.], dtype=float32),
 array([[-0.892862  ],
        [ 0.7337135 ],
        [-0.05061114],
        [-0.97688395]], dtype=float32),
 array([0.], dtype=float32)]

6.2.2. 参数绑定

通常,我们希望在多个层之间共享参数。让我们看看如何优雅地做到这一点。在下文中,我们分配一个全连接层,然后专门使用它的参数来设置另一层的参数。在这里,我们需要在访问参数之前运行前向传播 net(X)

# We need to give the shared layer a name so that we can refer to its
# parameters
shared = nn.LazyLinear(8)
net = nn.Sequential(nn.LazyLinear(8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.LazyLinear(1))

net(X)
# Check whether the parameters are the same
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# Make sure that they are actually the same object rather than just having the
# same value
print(net[2].weight.data[0] == net[4].weight.data[0])
tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])
net = nn.Sequential()
# We need to give the shared layer a name so that we can refer to its
# parameters
shared = nn.Dense(8, activation='relu')
net.add(nn.Dense(8, activation='relu'),
        shared,
        nn.Dense(8, activation='relu', params=shared.params),
        nn.Dense(10))
net.initialize()

X = np.random.uniform(size=(2, 20))

net(X)
# Check whether the parameters are the same
print(net[1].weight.data()[0] == net[2].weight.data()[0])
net[1].weight.data()[0, 0] = 100
# Make sure that they are actually the same object rather than just having the
# same value
print(net[1].weight.data()[0] == net[2].weight.data()[0])
[ True  True  True  True  True  True  True  True]
[ True  True  True  True  True  True  True  True]
# We need to give the shared layer a name so that we can refer to its
# parameters
shared = nn.Dense(8)
net = nn.Sequential([nn.Dense(8), nn.relu,
                     shared, nn.relu,
                     shared, nn.relu,
                     nn.Dense(1)])

params = net.init(jax.random.PRNGKey(d2l.get_seed()), X)

# Check whether the parameters are different
print(len(params['params']) == 3)
True
# tf.keras behaves a bit differently. It removes the duplicate layer
# automatically
shared = tf.keras.layers.Dense(4, activation=tf.nn.relu)
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    shared,
    shared,
    tf.keras.layers.Dense(1),
])

net(X)
# Check whether the parameters are different
print(len(net.layers) == 3)
True

这个例子表明,第二层和第三层的参数是绑定的。它们不仅仅是相等的,它们是由完全相同的张量表示的。因此,如果我们改变其中一个参数,另一个也会改变。

您可能想知道,当参数被绑定时,梯度会发生什么?由于模型参数包含梯度,因此在反向传播期间,第二个隐藏层和第三个隐藏层的梯度会相加。

您可能想知道,当参数被绑定时,梯度会发生什么?由于模型参数包含梯度,因此在反向传播期间,第二个隐藏层和第三个隐藏层的梯度会相加。

您可能想知道,当参数被绑定时,梯度会发生什么?由于模型参数包含梯度,因此在反向传播期间,第二个隐藏层和第三个隐藏层的梯度会相加。

6.2.3. 小结

我们有多种方式来访问和绑定模型参数。

6.2.4. 练习

  1. 使用 6.1节 中定义的 NestMLP 模型,并访问各个层的参数。

  2. 构建一个包含共享参数层的多层感知机并进行训练。在训练过程中,观察每一层的模型参数和梯度。

  3. 为什么共享参数是个好主意?