我在学习Pyro的预测入门教程,并试图在模型训练后访问学习到的参数时,发现使用不同的访问方法会得到一些参数的不同结果(而其他参数的结果相同)。
以下是教程中简化的可重现代码:
import torchimport pyroimport pyro.distributions as distfrom pyro.contrib.examples.bart import load_bart_odfrom pyro.contrib.forecast import ForecastingModel, Forecasterpyro.enable_validation(True)pyro.clear_param_store()pyro.__version__# '1.3.1'torch.__version__# '1.5.0+cu101'# 导入并准备数据dataset = load_bart_od()T, O, D = dataset["counts"].shapedata = dataset["counts"][:T // (24 * 7) * 24 * 7].reshape(T // (24 * 7), -1).sum(-1).log()data = data.unsqueeze(-1)T0 = 0 # 开始T2 = data.size(-2) # 结束T1 = T2 - 52 # 训练/测试分割# 定义模型类class Model1(ForecastingModel): def model(self, zero_data, covariates): data_dim = zero_data.size(-1) feature_dim = covariates.size(-1) bias = pyro.sample("bias", dist.Normal(0, 10).expand([data_dim]).to_event(1)) weight = pyro.sample("weight", dist.Normal(0, 0.1).expand([feature_dim]).to_event(1)) prediction = bias + (weight * covariates).sum(-1, keepdim=True) assert prediction.shape[-2:] == zero_data.shape noise_scale = pyro.sample("noise_scale", dist.LogNormal(-5, 5).expand([1]).to_event(1)) noise_dist = dist.Normal(0, noise_scale) self.predict(noise_dist, prediction)# 拟合模型pyro.set_rng_seed(1)pyro.clear_param_store()time = torch.arange(float(T2)) / 365covariates = torch.stack([time], dim=-1)forecaster = Forecaster(Model1(), data[:T1], covariates[:T1], learning_rate=0.1)
到目前为止,一切顺利;现在,我想要检查存储在Paramstore
中的学习到的潜在参数。似乎有不止一种方法可以做到这一点;使用get_all_param_names()
方法:
for name in pyro.get_param_store().get_all_param_names(): print(name, pyro.param(name).data.numpy())
我得到
AutoNormal.locs.bias [14.585433]AutoNormal.scales.bias [0.00631594]AutoNormal.locs.weight [0.11947815]AutoNormal.scales.weight [0.00922901]AutoNormal.locs.noise_scale [-2.0719821]AutoNormal.scales.noise_scale [0.03469057]
但是使用named_parameters()
方法:
pyro.get_param_store().named_parameters()
对于位置(locs
)参数给出相同的值,但所有scales
参数给出不同的值:
dict_items([('AutoNormal.locs.bias', Parameter containing: tensor([14.5854], requires_grad=True)), ('AutoNormal.scales.bias', Parameter containing: tensor([-5.0647], requires_grad=True)), ('AutoNormal.locs.weight', Parameter containing: tensor([0.1195], requires_grad=True)), ('AutoNormal.scales.weight', Parameter containing: tensor([-4.6854], requires_grad=True)),('AutoNormal.locs.noise_scale', Parameter containing: tensor([-2.0720], requires_grad=True)), ('AutoNormal.scales.noise_scale', Parameter containing: tensor([-3.3613], requires_grad=True))])
这是怎么可能的?根据文档,Paramstore
是一个简单的键值存储;其中只有这六个键:
pyro.get_param_store().get_all_param_names() # .keys()方法给出相同的结果# 结果dict_keys(['AutoNormal.locs.bias','AutoNormal.scales.bias', 'AutoNormal.locs.weight', 'AutoNormal.scales.weight', 'AutoNormal.locs.noise_scale', 'AutoNormal.scales.noise_scale'])
因此,不可能一种方法访问一组项目而另一种方法访问另一组项目。
我在这里错过了什么吗?
回答:
情况如下,正如我在与这个问题并行打开的Github线程中所揭示的…
Paramstore
不再仅仅是一个简单的键值存储 – 它还执行约束转换;引用上述链接中的一位Pyro开发者的说法:
这里有一些历史背景。
ParamStore
最初只是一个键值存储。然后我们添加了对受约束参数的支持;这引入了用户面对的受约束值和内部不受约束值之间的新层次分离。我们创建了一个新的类似字典的用户面对接口,只暴露受约束的值,但为了保持与旧代码的向后兼容性,我们保留了旧的接口。两个接口在源文件中是区分的[…]但正如你所观察到的,看起来我们忘记了将旧接口标记为已弃用。我猜在澄清文档时,我们应该:
澄清ParamStore不再是一个简单的键值存储,而是也执行约束转换;
将所有“旧”风格的接口方法标记为已弃用;
从示例和教程中删除“旧”风格的接口使用。
因此,事实证明,虽然pyro.param()
在受约束(用户面对)的空间中返回结果,但较旧的方法named_parameters()
返回不受约束的(即仅供内部使用)的值,因此出现了明显的差异。
的确不难验证两个方法返回的scales
值确实通过对数变换相关:
import numpy as npitems = list(pyro.get_param_store().named_parameters()) # 不受约束的空间i = 0for name in pyro.get_param_store().keys(): if 'scales' in name: temp = np.log( pyro.param(name).item() # 受约束的空间 ) print(temp, items[i][1][0].item() , np.allclose(temp, items[i][1][0].item())) i+=1# 结果:-5.027793402915326 -5.0277934074401855 True-4.600319371162187 -4.6003193855285645 True-3.3920585732532835 -3.3920586109161377 True
为什么这种差异只影响scales
参数?那是因为scales
(即本质上是方差)根据定义被约束为正值;这对locs
(即均值)不成立,它们不受约束,因此两种表示对它们来说是相同的。
作为上述问题的结果,现在在Paramstore
的文档中添加了一个新的项目,提供了一个相关的提示:
一般来说,参数与受约束和不受约束的值相关。例如,在幕后,一个被约束为正值的参数在对数空间中表示为不受约束的张量。
以及在旧接口的named_parameters()
方法的文档中:
请注意,如果参数受到约束,unconstrained_value处于约束隐式使用的未受约束空间中。