层和块 之前⾸次介绍神经⽹络时,我们关注的是具有单⼀输出的线性模型。在这⾥,整个模型只有⼀个输出。 注意,单个神经⽹络
接受⼀些输⼊;
⽣成相应的标量输出;
具有⼀组相关 参数(parameters),更新这些参数可以优化某⽬标函数
当考虑具有多个输出的⽹络时,我们利⽤⽮量化算法来描述整层神经元。 像单个神经元⼀样,层
接受⼀组输⼊
⽣成相应的输出
由⼀组可调整参数描述。
为了实现更复杂的⽹络,我们引⼊了神经⽹络块的概念。块(block)可以描述单个层、由多个层组成的组件或整个模型本⾝。使⽤块进⾏抽象的⼀个好处是可以将⼀些块组合成更⼤的组件,这⼀过程通常是递归的,
从编程的⻆度来看,块由类(class)表⽰。它的任何⼦类都必须定义⼀个将其输⼊转换为输出的前向传播函数,并且必须存储任何必需的参数。注意,有些块不需要任何参数。最后,为了计算梯度,块必须具有反向传播函数。在定义我们⾃⼰的块时,由于⾃动微分提供了⼀些后端实现,我们只需要考虑前向传播函数和必需的参数。
在构造自定义块之前,我们先回顾一下多层感知机的代码。下面的代码生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接隐藏层,然后是一个具有10个隐藏单元且不带激活函数的全连接输出层。
1 2 3 4 5 6 7 8 9 10 import torchfrom torch import nnfrom torch.nn import functional as Fnet = nn.Sequential(nn.Linear(20 , 256 ), nn.ReLU(), nn.Linear(256 , 10 )) X = torch.rand(2 , 20 ) net(X)
tensor([[ 0.0245, -0.0111, -0.0928, -0.0953, -0.0050, 0.0019, -0.1171, -0.1416,
0.0784, 0.0440],
[ 0.0434, -0.0726, -0.1085, -0.0335, 0.0189, -0.0921, -0.0794, -0.1732,
-0.0648, -0.0184]], grad_fn=<AddmmBackward>)
nn.Sequential定义了一种特殊的Module,即在Pytorch中表示一个块的类,它维护了一个有Module组成的有序列表。注意,两个全连接层都是Linear类的实例,Linear类本身就算Module的子类。 在调用模型时直接使用的是net(X)获得模型的输出,这实际上是运用了魔法函数__call__(X)
自定义块 想要直观地了解块是如何工作的,最简单的方法就是自己实现一个。在实现我们自定义块之前,我们简单总结一下每个块必须提供的基本功能:
将输入数据作为其前向传播函数的参数。
通过前向传播函数来生成输出。
计算其输出关于输入的梯度,可通过其反向传播函数进行访问。
存储和访问当前传播计算所需的参数。
根据需要初始化模型参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 class MLP (nn.Module): def __init__ (self ): super ().__init__() self.hidden = nn.Linear(20 , 256 ) self.out = nn.Linear(256 , 10 ) def forward (self, X ): return self.out(F.relu(self.hidden(X)))
tensor([[ 0.1098, 0.1047, 0.0625, -0.1535, 0.0845, 0.2014, -0.2848, -0.0389,
-0.2126, 0.2409],
[ 0.0870, 0.0876, 0.1605, -0.0286, 0.1187, 0.0536, -0.1739, 0.0941,
-0.0759, 0.1876]], grad_fn=<AddmmBackward>)
块的一个主要优点是它的多功能性。我们可以子类化块以创建层(如全连接层的类)、整个模型(如上面的MLP类)或具有重点复杂度的各种组件。
顺序块 现在我们可以更仔细地看看Sequential类是如何工作的,回想一下Sequential的设计是为了把其他模块串起来。为了构建我们自己的简化的MySequential,我们只需要定义两个关键函数:
一种将块逐个追加到列表中的函数。
一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
1 2 3 4 5 6 7 8 9 10 11 12 13 class MySequential (nn.Module): def __init__ (self, *args ): super ().__init__() for idx, module in enumerate (args): self._modules[str (idx)] = module def forward (self, X ): for block in self._modules.values(): X = block(X) return X
__init__
函数将每个模块逐个添加到有序字典_modules中。你可能会好奇为什么每个Module都有一个_modules属性?以及为什么我们使用它而不是自己定义一个Python列表?简而言之,_modules的主要优点是:在模块的参数初始化过程中,系统知道在_modules字典中查找需要初始化参数的子块。
1 2 net = MySequential(nn.Linear(20 , 256 ), nn.ReLU(), nn.Linear(256 , 10 )) net(X)
tensor([[-0.0161, 0.1370, -0.1066, -0.1052, -0.0573, 0.0281, 0.1026, 0.0446,
0.1310, 0.0329],
[-0.0654, 0.1250, 0.1760, -0.0270, -0.1305, 0.1387, 0.0318, 0.0599,
0.1033, 0.0471]], grad_fn=<AddmmBackward>)
在前向传播函数中执行代码 Sequential类使模型构造变得简单,允许我们组合新的架构,⽽不必定义⾃⼰的类。然⽽,并不是所有的架构都是简单的顺序架构。当需要更强的灵活性时,我们需要定义⾃⼰的块。例如,我们可能希望在前向传播函数中执⾏Python的控制流。此外,我们可能希望执⾏任意的数学运算,⽽不是简单地依赖预定义的神经⽹络层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class FixedHiddenMLP (nn.Module): def __init__ (self ): super ().__init__() self.rand_weight = torch.rand((20 , 20 ), requires_grad=False ) self.linear = nn.Linear(20 , 20 ) def forward (self, X ): X = self.linear(X) X = F.relu(torch.mm(X, self.rand_weight) + 1 ) X = self.linear(X) while X.abs ().sum () > 1 : X /= 2 return X.sum ()
在返回输出之前,模型做了⼀些不寻常的事情:它运⾏了⼀个while循环,在L 1 范数⼤于1的条件下,将输出向量除以2,直到它满⾜条件为⽌。最后,模型返回了X中所有项的和。注意,此操作可能不会常⽤于在任何实际任务中,只是向你展⽰如何将任意代码集成到神经⽹络计算的流程中。
1 2 net = FixedHiddenMLP() net(X)
tensor(0.0529, grad_fn=<SumBackward0>)
我们还可以混合搭配各种组合块的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class NestMLP (nn.Module): def __init__ (self ): super ().__init__() self.net = nn.Sequential(nn.Linear(20 , 64 ), nn.ReLU(), nn.Linear(64 , 32 ), nn.ReLU()) self.linear = nn.Linear(32 , 16 ) def forward (self, X ): return self.linear(self.net(X)) chimera = nn.Sequential(NestMLP(), nn.Linear(16 , 20 ), FixedHiddenMLP()) chimera(X)
tensor(-0.0705, grad_fn=<SumBackward0>)
效率 你可能会开始担⼼操作效率的问题。毕竟,我们在⼀个⾼性能的深度学习库中进⾏了⼤量的字典查找、代码执⾏和许多其他的Python代码。Python的问题全局解释器锁是众所周知的。在深度学习环境中,我们担⼼速度极快的GPU可能要等到CPU运⾏Python代码后才能运⾏另⼀个作业。
参数管理 在选择了架构并设置了超参数后,我们就进⼊了训练阶段。此时,我们的⽬标是找到使损失函数最⼩化的模型参数值。经过训练后,我们将需要使⽤这些参数来做出未来的预测。此外,有时我们希望提取参数,以便在其他环境中复⽤它们,将模型保存下来,以便它可以在其他软件中执⾏,或者为了获得科学的理解⽽进⾏检查。
之前的介绍中,我们只依靠深度学习框架来完成训练的⼯作,⽽忽略了操作参数的具体细节。本节,我们将 介绍以下内容:
访问参数,⽤于调试、诊断和可视化。
参数初始化。
在不同模型组件间共享参数。
参数访问 1 2 3 4 5 import torchfrom torch import nnnet = nn.Sequential(nn.Linear(4 , 8 ), nn.ReLU(), nn.Linear(8 , 1 )) X = torch.rand(size=(2 , 4 )) net(X)
tensor([[-0.2628],
[-0.4082]], grad_fn=<AddmmBackward>)
我们从已有模型中访问参数。当通过Sequential类 定义模型时,我们可以通过索引来访问模型的任意层。这就像模型是⼀个列表⼀样,每层的参数都在其属性中。如下所⽰,我们可以检查第⼆个全连接层的参数。
1 print (net[2 ].state_dict())
OrderedDict([('weight', tensor([[-0.1541, -0.2901, 0.0169, -0.2310, -0.1171, 0.2987, -0.2756, 0.3078]])), ('bias', tensor([-0.1445]))])
输出的结果告诉我们⼀些重要的事情:⾸先,这个全连接层包含两个参数,分别是该层的权重和偏置。两者都存储为单精度浮点数(float32)。注意,参数名称允许唯⼀标识每个参数,即使在包含数百个层的⽹络中也是如此。
目标参数 注意,每个参数都表⽰为参数类的⼀个实例。要对参数执⾏任何操作,⾸先我们需要访问底层的数值。有⼏种⽅法可以做到这⼀点。有些⽐较简单,⽽另⼀些则⽐较通⽤。下⾯的代码从第⼆个全连接层(即第三个神经⽹络层)提取偏置,提取后返回的是⼀个参数类实例,并进⼀步访问该参数的值。
1 2 3 print (type (net[2 ].bias))print (net[2 ].bias)print (net[2 ].bias.data)
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([-0.1445], requires_grad=True)
tensor([-0.1445])
参数是复合的对象,包含值、梯度和额外信息。这就是我们需要显式参数值的原因。除了值之外,我们还可以访问每个参数的梯度。在上⾯这个⽹络中,由于我们还没有调⽤反向传播,所以参数的梯度处于初始状态。
1 net[2 ].weight.grad == None
True
一次访问所有参数 当我们需要对所有参数执⾏操作时,逐个访问它们可能会很⿇烦。当我们处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂,因为我们需要递归整个树来提取每个⼦块的参数。下⾯,我们将通过演⽰来⽐较访问第⼀个全连接层的参数和访问所有层。
1 2 print (*[(name, param.shape) for name, param in net[0 ].named_parameters()])
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
1 2 print (*[(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]))
这为我们提供了另⼀种访问⽹络参数的⽅式,如下所⽰。
1 net.state_dict()['2.weight' ].data
tensor([[-0.1541, -0.2901, 0.0169, -0.2310, -0.1171, 0.2987, -0.2756, 0.3078]])
从嵌套块收集参数 让我们看看,如果我们将多个块相互嵌套,参数命名约定是如何⼯作的。我们⾸先定义⼀个⽣成块的函数(可以说是“块⼯⼚”),然后将这些块组合到更⼤的块中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def block1 (): return nn.Sequential(nn.Linear(4 , 8 ), nn.ReLU(), nn.Linear(8 , 4 ), nn.ReLU()) def block2 (): net = nn.Sequential() for i in range (4 ): net.add_module(f'block{i} ' , block1()) return net rgnet = nn.Sequential(block2(), nn.Linear(4 , 1 )) rgnet(X)
tensor([[-0.2735],
[-0.2735]], grad_fn=<AddmmBackward>)
设计了⽹络后,我们看看它是如何⼯作的。
Sequential(
(0): Sequential(
(block0): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block1): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block2): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block3): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
)
(1): Linear(in_features=4, out_features=1, bias=True)
)
因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引⼀样访问它们。下⾯,我们访问第⼀个主要的块中、第⼆个⼦块的第⼀层的偏置项。
1 rgnet[0 ][1 ][0 ].bias.data
tensor([-0.1550, -0.0942, -0.2024, -0.2669, 0.0101, 0.3473, -0.3262, -0.0965])
参数初始化 知道了如何访问参数后,现在我们看看如何正确地初始化参数。我们在 4.8节中讨论了良好初始化的必要性。深度学习框架提供默认随机初始化,也允许我们创建⾃定义初始化方法,满⾜我们通过其他规则实现初始化权重。
默认情况下,PyTorch会根据⼀个范围均匀地初始化权重和偏置矩阵,这个范围是根据输⼊和输出维度计算出的。PyTorch的nn.init模块提供了多种预置初始化⽅法。
内置初始化 让我们首先调⽤内置的初始化器。 下⾯的代码将所有权重参数初始化为标准差为0.01的⾼斯随机变量 ,且将偏置参数设置为0。
1 2 3 4 5 6 def init_normal (m ): if type (m) == nn.Linear: nn.init.normal_(m.weight, mean=0 , std=0.01 ) nn.init.zeros_(m.bias) net.apply(init_normal) net[0 ].weight.data[0 ], net[0 ].bias.data[0 ]
(tensor([ 0.0167, 0.0012, -0.0071, 0.0068]), tensor(0.))
将所有参数初始化为给定的常数 ,⽐如初始化为1。
1 2 3 4 5 6 def init_constant (m ): if type (m) == nn.Linear: nn.init.constant_(m.weight, 1 ) nn.init.zeros_(m.bias) net.apply(init_constant) net[0 ].weight.data[0 ], net[0 ].bias.data[0 ]
(tensor([1., 1., 1., 1.]), tensor(0.))
还可以对某些块应⽤不同的初始化方法 。
例如,下⾯我们使⽤Xavier初始化⽅法初始化第⼀个神经⽹络层,然后将第三个神经⽹络层初始化为常量值42。
1 2 3 4 5 6 7 def xavier (m ): if type (m) == nn.Linear: nn.init.xavier_uniform_(m.weight) def init_42 (m ): if type (m) == nn.Linear: nn.init.constant_(m.weight, 42 )
1 2 3 4 net[0 ].apply(xavier) net[2 ].apply(init_42) print (net[0 ].weight.data[0 ])print (net[2 ].weight.data)
tensor([ 0.5670, -0.0233, 0.1025, -0.4254])
tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
自定义初始化 有时,深度学习框架没有提供我们需要的初始化⽅法。在下⾯的例⼦中,我们使⽤以下的分布为任意权重参数$w$定义初始化⽅法:
同样,我们实现了一个my_init函数来应用到net
1 2 3 4 5 6 7 8 def my_init (m ): if type (m) == nn.Linear: print ("Init" , *[(name, param.shape) for name, param in m.named_parameters()][0 ]) nn.init.uniform_(m.weight, -10 , 10 ) m.weight.data *= m.weight.data.abs () >= 5 net.apply(my_init) net[0 ].weight[:2 ]
Init weight torch.Size([8, 4])
Init weight torch.Size([1, 8])
tensor([[ 0.0000, 6.1604, -0.0000, -0.0000],
[-0.0000, -6.2209, 0.0000, -6.2103]], grad_fn=<SliceBackward>)
注意,我们始终可以直接设置参数。
1 2 3 net[0 ].weight.data[:] += 1 net[0 ].weight.data[0 , 0 ] = 42 net[0 ].weight.data[0 ]
tensor([42.0000, 7.1604, 1.0000, 1.0000])
参数绑定 有时我们希望在多个层间共享参数:我们可以定义⼀个稠密层,然后使⽤它的参数来设置另⼀个层的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 shared = nn.Linear(8 , 8 ) net = nn.Sequential(nn.Linear(4 , 8 ), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8 , 1 )) print (net)net(X) print (net[2 ].weight.data[0 ] == net[4 ].weight.data[0 ])net[2 ].weight.data[0 , 0 ] = 100 print ('net[4]结果:' , net[4 ].weight.data[0 , 0 ])print (net[2 ].weight.data[0 ] == net[4 ].weight.data[0 ])
Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=8, bias=True)
(3): ReLU()
(4): Linear(in_features=8, out_features=8, bias=True)
(5): ReLU()
(6): Linear(in_features=8, out_features=1, bias=True)
)
tensor([True, True, True, True, True, True, True, True])
net[4]结果: tensor(100.)
tensor([True, True, True, True, True, True, True, True])
这个例⼦表明第三个和第五个神经⽹络层的参数是绑定的。它们不仅值相等,⽽且由相同的张量表⽰。因此,如果我们改变其中⼀个参数,另⼀个参数也会改变。你可能会思考:当参数绑定时,梯度会发⽣什么情况?答案是由于模型参数包含梯度,因此在反向传播期间第⼆个隐藏层(即第三个神经⽹络层)和第三个隐藏层(即第五个神经⽹络层)的梯度会加在⼀起。
延后初始化 到目前为止,我们忽略了建立网络时需要做的以下这些事情:
我们定义了网络架构,但没有指定输入维度
我们添加层时没有指定前一层的输出维度
我们在初始化参数时,甚至没有足够的信息来确定模型应该包含多少参数
你可能会对我们的代码能运行感到惊讶。毕竟,深度学习框架无法判断网络的输入维度是什么。这里的诀窍是框架的延后初始化,即直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小。
实例化网络 nn.LazyLinear:延后初始化,全连接层
1 2 3 net = nn.Sequential(nn.LazyLinear(256 ), nn.ReLU(), nn.Linear(256 , 10 ))
Sequential(
(0): LazyLinear(in_features=0, out_features=256, bias=True)
(1): ReLU()
(2): Linear(in_features=256, out_features=10, bias=True)
)
Uninitialized parameter
tensor([[ 0.2092, -0.2179, -0.0044, 0.1780, -0.2131, 0.1658, 0.3004, -0.0157,
-0.1154, -0.3769],
[ 0.3296, -0.0757, 0.0329, 0.0812, -0.1362, 0.2927, 0.0773, 0.0638,
-0.0448, -0.2469]], grad_fn=<AddmmBackward>)
Parameter containing:
tensor([[-0.1225, -0.1422, 0.3074, 0.3573],
[-0.2188, 0.0541, 0.0414, 0.0416],
[-0.4397, -0.2238, -0.4481, -0.1612],
...,
[ 0.1303, 0.1513, 0.0017, -0.3045],
[-0.4023, 0.0995, -0.3202, 0.1096],
[ 0.2366, 0.3432, 0.1951, 0.2831]], requires_grad=True)
自定义层 深度学习成功背后的⼀个因素是神经⽹络的灵活性:我们可以⽤创造性的⽅式组合不同的层,从⽽设计出适⽤于各种任务的架构。例如,研究⼈员发明了专⻔⽤于处理图像、⽂本、序列数据和执⾏动态规划的层。未来,你会遇到或要⾃⼰发明⼀个现在在深度学习框架中还不存在的层。在这些情况下,你必须构建⾃定义层。在本节中,我们将向你展⽰如何构建。
不带参数的层 ⾸先,我们构造⼀个没有任何参数的⾃定义层。如果你还记得我们对块的介绍,这应该看起来很眼熟。下⾯的CenteredLayer类要从其输⼊中减去均值。要构建它,我们只需继承基础层类并实现前向传播功能
1 2 3 import torchimport torch.nn.functional as Ffrom torch import nn
1 2 3 4 5 6 class CenteredLayer (nn.Module): def __init__ (self ): super ().__init__() def forward (self, X ): return X - X.mean()
1 2 layer = CenteredLayer() layer(torch.FloatTensor([1 , 2 , 3 , 4 , 5 ]))
tensor([-2., -1., 0., 1., 2.])
我们可以将层作为组件合并到更复杂的模型中
1 net = nn.Sequential(nn.Linear(8 , 128 ), CenteredLayer())
作为额外的健全性检查,我们可以在向该网络发送随机数据后,检查均值是否为0。由于我们处理的是浮点数,因为存储精度的原因,我们仍然可能会看到一个非常小的非零数。
1 2 Y = net(torch.rand(4 , 8 )) Y.mean()
tensor(-9.3132e-09, grad_fn=<MeanBackward0>)
带参数的层 上我们知道了如何定义简单的层,下⾯我们继续定义具有参数的层,这些参数可以通过训练进⾏调整。我们可以使⽤内置函数来创建参数,这些函数提供⼀些基本的管理功能。⽐如管理访问、初始化、共享、保存和加载模型参数。这样做的好处之⼀是:我们不需要为每个⾃定义层编写⾃定义的序列化程序。
现在,让我们实现⾃定义版本的全连接层。回想⼀下,该层需要两个参数,⼀个⽤于表⽰权重,另⼀个⽤于表⽰偏置项。在此实现中,我们使⽤修正线性单元作为激活函数。该层需要输⼊参数:in_units和units,分别表⽰输⼊数和输出数。
1 2 3 4 5 6 7 8 9 class MyLinear (nn.Module): def __init__ (self, in_units, units ): super ().__init__() self.weight = nn.Parameter(torch.randn(in_units, units)) self.bias = nn.Parameter(torch.randn(units,)) def forward (self, X ): linear = torch.matmul(X, self.weight.data) + self.bias.data return F.relu(linear)
1 2 linear = MyLinear(5 , 3 ) linear.weight
Parameter containing:
tensor([[ 1.0367, 0.7462, 2.7232],
[ 0.1762, 2.0692, -0.8030],
[ 2.1293, -0.1297, 0.3912],
[-0.4883, -0.2507, -0.5120],
[-0.5646, 0.6213, 1.0068]], requires_grad=True)
我们可以使用自定义层直接执行前向传播计算
1 linear(torch.rand(2 , 5 ))
tensor([[0.0000, 1.9341, 2.1127],
[0.0000, 1.4290, 0.2516]])
我们还可以使用自定义层构建模型,就像使用内置的全连接层一样使用自定义层
1 2 net = nn.Sequential(MyLinear(64 , 8 ), MyLinear(8 , 1 )) net(torch.rand(2 , 64 ))
tensor([[0.7789],
[0.0000]])
读写文件 对于单个张量,我们可以直接调用load和save函数分别读写它们。 1 2 3 import torchfrom torch import nnimport torch.nn.functional as F
1 2 x = torch.arange(4 ) torch.save(x, 'x-file' )
我们现在可以将存储在文件中的数据读回内存
1 2 x2 = torch.load('x-file' ) x2
tensor([0, 1, 2, 3])
我们可以存储一个张量列表,然后把它们读回内存
1 2 y = torch.zeros(4 ) torch.save([x, y], 'x-files' )
1 2 x2, y2 = torch.load('x-files' ) x2, y2
(tensor([0, 1, 2, 3]), tensor([0., 0., 0., 0.]))
我们甚至可以写入或读取从字符串映射到张量的字典。当我们要读取或写入模型中的所有权重时,这很方便
1 2 mydict = {'x' : x, 'y' : y} torch.save(mydict, 'mydict' )
1 2 mydict2 = torch.load('mydict' ) mydict2
{'x': tensor([0, 1, 2, 3]), 'y': tensor([0., 0., 0., 0.])}
加载和保存模型参数 1 2 3 4 5 6 7 8 class MLP (nn.Module): def __init__ (self ): super ().__init__() self.hidden = nn.Linear(20 , 256 ) self.output = nn.Linear(256 , 10 ) def forward (self, x ): return self.output(F.relu(self.hidden(x)))
1 2 3 net = MLP() X = torch.randn(size=(2 , 20 )) Y = net(X)
1 2 torch.save(net.state_dict(), 'mlp.params' )
1 2 3 4 clone = MLP() clone.load_state_dict(torch.load('mlp.params' )) clone.eval ()
MLP(
(hidden): Linear(in_features=20, out_features=256, bias=True)
(output): Linear(in_features=256, out_features=10, bias=True)
)
由于两个模型参数相同,所以得到的预测值应该一样,可以验证一下
1 2 Y_clone = clone(X) Y_clone == Y
tensor([[True, True, True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True, True, True]])
发现预测结果确实全部都一样
GPU
Tue Aug 9 15:27:18 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 472.19 Driver Version: 472.19 CUDA Version: 11.4 |
|-------------------------------+----------------------+----------------------+
| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 NVIDIA GeForce ... WDDM | 00000000:01:00.0 Off | N/A |
| N/A 42C P8 N/A / N/A | 365MiB / 2048MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 1544 C+G ...2txyewy\TextInputHost.exe N/A |
| 0 N/A N/A 2132 C+G ...5n1h2txyewy\SearchApp.exe N/A |
| 0 N/A N/A 6704 C+G Insufficient Permissions N/A |
| 0 N/A N/A 6844 C+G ...d\runtime\WeChatAppEx.exe N/A |
| 0 N/A N/A 12076 C+G ...m Files\SDKDNS\SDKDNS.exe N/A |
| 0 N/A N/A 16400 C+G Insufficient Permissions N/A |
| 0 N/A N/A 21108 C+G ...kyb3d8bbwe\HxAccounts.exe N/A |
| 0 N/A N/A 27024 C+G Insufficient Permissions N/A |
| 0 N/A N/A 28520 C+G ...cw5n1h2txyewy\LockApp.exe N/A |
| 0 N/A N/A 31328 C+G ...tracted\WechatBrowser.exe N/A |
+-----------------------------------------------------------------------------+
在PyTorch中,每个数组都有一个设备(device),我们通常将其称为上下文(context)。默认情况下,所有变量和相关的计算都分配给CPU。有时上下文可能是GPU。当我们跨多个服务器部署作业时,事情会变得更加棘手。通过智能地将数组分配给上下文,我们可以最大限度地减少在设备之间传输数据的时间。例如,当在带有GPU的服务器上训练神经网络时,我们通常希望模型的参数在GPU上。
要运行此部分中的程序,至少需要俩个GPU。注意,对于大多数桌面计算机来说,这可能是奢侈的,但在云中很容易获得。
1 2 torch.cuda.is_available()
True
1 2 torch.cuda.device_count()
1
device(type='cuda')
我们可以查询张量所在的设备,默认情况下,张量存储在cpu中
1 2 x = torch.tensor([1 , 2 , 3 ]) x.device
device(type='cpu')
需要注意的是,⽆论何时我们要对多个项进⾏操作,它们都必须在同⼀个设备上。例如,如果我们对两个张量求和,我们需要确保两个张量都位于同⼀个设备上,否则框架将不知道在哪⾥存储结果,甚⾄不知道在哪⾥执⾏计算。
1 2 3 X = torch.ones(2 , 3 , device='cuda' ) X.device
device(type='cuda', index=0)
1 2 3 4 5 print ('转移前:' , x.device)z = x.to('cuda' ) print ('转移后:' , z.device)
转移前: cpu
转移后: cuda:0
1 2 3 4 5 print ('转移前:' , x.device)z = x.cuda() print ('转移后:' , z.device)
转移前: cpu
转移后: cuda:0
此方法也同样使用,当存在多张GPU时,如何在不同的GPU中进行数据的交互。 例如:将GPU0中的tensor转移到GPU1中。 Z = X.cuda(1)
tensor([1, 2, 3])
tensor([1., 1., 1.], device='cuda:0')
不同设备之间tensor是无法进行计算的
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_18704\1702998212.py in <module>
----> 1 x + xx
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!
要使用模型进行数据的计算,也必须将模型放入GPU中,同时保证Tensor也必须在GPU中
1 net = nn.Sequential(nn.Linear(3 , 1 ))
tensor([1.0507], grad_fn=<AddBackward0>)
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_18704\2159940700.py in <module>
----> 1 net(xx*1.0)
d:\users\python\anaconda3.8\envs\pytorch\lib\site-packages\torch\nn\modules\module.py in _call_impl(self, *input, **kwargs)
887 result = self._slow_forward(*input, **kwargs)
888 else:
--> 889 result = self.forward(*input, **kwargs)
890 for hook in itertools.chain(
891 _global_forward_hooks.values(),
d:\users\python\anaconda3.8\envs\pytorch\lib\site-packages\torch\nn\modules\container.py in forward(self, input)
117 def forward(self, input):
118 for module in self:
--> 119 input = module(input)
120 return input
121
d:\users\python\anaconda3.8\envs\pytorch\lib\site-packages\torch\nn\modules\module.py in _call_impl(self, *input, **kwargs)
887 result = self._slow_forward(*input, **kwargs)
888 else:
--> 889 result = self.forward(*input, **kwargs)
890 for hook in itertools.chain(
891 _global_forward_hooks.values(),
d:\users\python\anaconda3.8\envs\pytorch\lib\site-packages\torch\nn\modules\linear.py in forward(self, input)
92
93 def forward(self, input: Tensor) -> Tensor:
---> 94 return F.linear(input, self.weight, self.bias)
95
96 def extra_repr(self) -> str:
d:\users\python\anaconda3.8\envs\pytorch\lib\site-packages\torch\nn\functional.py in linear(input, weight, bias)
1751 if has_torch_function_variadic(input, weight):
1752 return handle_torch_function(linear, (input, weight), input, weight, bias=bias)
-> 1753 return torch._C._nn.linear(input, weight, bias)
1754
1755
RuntimeError: Tensor for argument #3 'mat2' is on CPU, but expected it to be on GPU (while checking arguments for addmm)
tensor([0.3924], device='cuda:0', grad_fn=<AddBackward0>)
总之,只要所有的数据和参数都在同⼀个设备上,我们就可以有效地学习模型。