6.2. 参数管理¶ 在 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. 小结¶
我们有多种方式来访问和绑定模型参数。