首次介绍神经网络时,关注的是具有单一输出的线性模型,整个模型只有一个输出。单个神经网络(1)接受一些输入;(2)生成相应的标量输出;(3)具有一组相关参数(parameters),更新这些参数可以优化目标函数。
对于具有多个输出的神经网络,我们利用矢量化算法来描述正层神经元。像单个神经元一样,层(1)接受一组输入;(2)生成相应的输出;(3)由一组可调整参数描述。
对于多层感知机,整个模型(1)接受原始输入(特征);(2)生成输出(预测);(3)并包含一些参数(所有组成层的参数集合)。同样,每个单独的层接受输入(由前一层提供),生成输出(到下一层输入),并且具有一组可调参数,这些参数根据从下一层反向传播的信号更新。
事实证明,研究讨论‘‘比单个层大’’但‘‘比整个模型小’’的组件更有价值。例如,在计算机视觉中广泛流行的ResNet-152架构就是有数百层,这些层是由层组(groups of layers)的重复模型组成。这个ResNet架构仍然是许多视觉任务的首选架构。在其他的领域,如自然语言处理,层组以各种重复模型排列的类似架构现在也是普遍存在。
为了实现这些复杂的网络,我们引入了神经网络块的概念。块(block)可以描述单个层、由多个层组成的组件或整个模型本身。抽象成块的好处是可以将一些块组合成更大的组件。这一过程通常是递归的。通过定义代码来按需生成任意复杂度的块,可以通过简洁的代码实现复杂的神经网络。
从编程的角度来看,块由类(class)表示。它的任何子类都必须定义一个将其输入转换为输出的前向传播函数,并且必须存储任何必须的参数(有些块不需要任何参数)。最后,为了计算梯度,块必须具有反向传播函数。
每个块必须提供的基本功能:
- 输入数据,作为其前向传播函数的输入参数。
- 输出数据,通过前向传播函数生成的输出参数。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。
- 存储和访问前向传播计算所需的参数。
- 根据需要初始化模型参数
from torch import nn
from torch import functional as F
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(784,256)
self.out = nn.Linear(256,10)
def forward(self, X):
return self.out(F.relu(self.hidden(X)))
import torch
X = torch.rand(10,784)
net = MLP()
net(X)
class SelfSequential(nn.Module):
def __init__(self, *args):
super().__init__()
for idx, module in enumerate(args):
# class Sequential(Module):
# _modules: Dict[str, Module] # type: ignore[assignment]
self._modules[str(idx)] = module
def forward(self, X):
for block in self._modules.values():
X = block(X)
return X
本节将介绍,(1)访问参数,用于调试、诊断和可视化;(2)参数初始化;(3)在不同模型组件间共享参数。
import torch
from torch import nn
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
X = torch.rand(size=(2,4))
net(X)
# tensor([[ 0.0034],
# [-0.0161]], grad_fn=)
通过Sequential类定义模型时,可以通过索引来访问模型的任意层。模型就像是一个列表一样,每层的参数都在其属性中。
由输出结果可知,首先,这个全连接层包含两个参数,分别是权重和偏置。两者都存储为单精度浮点数(float32)。注意,参数名称允许唯一标识每个参数,即使在包含数百层的网络中。
net[2].state_dict()
# OrderedDict([('weight',
# tensor([[ 0.2673, 0.0125, 0.1899, 0.2948, -0.1987, -0.2978, 0.2894, 0.3379]])),
# ('bias', tensor([-0.2959]))])
每个参数都表示为参数类的一个实例。要对参数执行任何操作,首先需要访问该参数。
参数是复合的对象,包含值、梯度和额外信息,这就是需要显示访问参数的原因。除值之外,还可以访问每个参数的梯度。
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
#
# Parameter containing:
# tensor([-0.2959], requires_grad=True)
# tensor([-0.2959])
print(net[2].weight.grad == None)
# True
当需要对所有参数执行操作时,逐个访问可能会很麻烦。当处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂,因为我们需要递归整个树来提取每个子块的参数。下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
# ('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
# ('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
另一种访问网络参数的方式
net.state_dict()['2.bias'].data
生成自定义组合块
def block1():
return nn.Sequential(nn.Linear(4,8), nn.ReLU(),nn.Linear(4,8), 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.7421],
# [-0.7421]], grad_fn=)
print(rgnet)
# Output exceeds the size limit. Open the full output data in a text editor
# Sequential(
# (0): Sequential(
# (block 0): 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()
# )
# (block 1): 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()
# )
# (block 2): 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()
# )
# (block 3): 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)
# )
层是分层嵌套的,可以像通过嵌套列表索引一样访问参数。
# 访问第一个主要块中第二个子块的第一层的偏置项
rgnet[0][1][0].bias.data
# tensor([ 0.4368, -0.4383, -0.2301, 0.1096, -0.0355, 0.2192, -0.4385, 0.0027])
默认情况,PyTorch根据一个范围均匀地初始化权重和偏置矩阵,范围由根据输入和输出维度计算得到。PyTorch的nn.init模块提供多种预置初始化方法。
import torch
from torch import nn
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))X = torch.rand(size=(2, 4))
X = torch.rand(size=(2, 4))
通过内置初始化器进行初始化
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.0041, 0.0010, 0.0083, 0.0200]), tensor(0.))
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.))
def init_xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_constant_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(init_xavier)
net[2].apply(init_constant_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
# tensor([-0.4327, -0.5751, -0.5363, 0.2745])
# tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
def my_init(m):
if type(m) == nn.Linear:
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight[:2]
# tensor([[-0.0000, 9.2375, 6.7505, -0.0000],
# [ 0.0000, -6.8064, 0.0000, 8.1219]], grad_fn=)
直接设置参数
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
# tensor([42.0000, 10.2375, 7.7505, 1.0000])
在多个层间共享参数,定义稠密层,使用该层参数初始化另一层的参数
shared = nn.Linear(8,8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同⼀个对象,⽽不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])
定义CenteredLayer类,从其输⼊中减去均值
def CenteredLayer(nn.Module):
def __init__(self):
super().__init__()
def forward(self, X):
return X - X.mean()
# 测试
layer = CenteredLayer()
layer(torch.FloatTensor([_ for _ in range(5)]))
# tensor([-2., -1., 0., 1., 2.])
以将自定义层作为组件合并到更复杂的模型中。
net = nn.Sequential(nn.Linear(8,128), CenteredLayer())
# 测试
Y = net(torch.rand(4,8))
Y.mean()
# tensor(-1.8626e-09, grad_fn=)
自定义全连接层,包含两个参数:权重和偏置。使⽤修正线性单元作为激活函数。输⼊参数:in_units和units,分别表⽰输⼊数和输出数。
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)
linear = MyLinear(5, 3)
linear.weight
# Parameter containing:
# tensor([[-0.1539, 0.7556, 0.5985],
# [-0.4069, 1.0454, -0.3202],
# [-1.2697, 2.2512, -0.1835],
# [ 1.5846, 0.3276, -0.8318],
# [-1.2247, -0.5247, 0.4574]], requires_grad=True)
像使⽤内置的全连接层⼀样使⽤⾃定义层
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))
# tensor([[0.],
# [0.]])
- 可以通过基本层类灵活地设计自定义层,新定义层的行为与深度学习框架中的现有层相同。
- 自定义层可以在任何环境和网络框架中调用。
- 层可以有局部参数,这些参数可以通过内置函数创建。
import torch
from torch import nn
from torch.nn import functional as F
x = torch.arange(5)
x
# tensor([0, 1, 2, 3, 4])
torch.arange(end: Number, step: Number,dtype:Optional[_dtype]=None),从0开始到end结束,步长为step
torch.arange(start: Number, end: Number, step: Number,dtype:Optional[_dtype]=None),默认步长为1,默认数据类型为整数
读写单个张量
torch.save(X, 'x-file')
x2 = torch.load('x-file')
x2
# tensor([0, 1, 2, 3, 4])
读写一个张量列表
y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)
# (tensor([0, 1, 2, 3, 4]), tensor([0., 0., 0., 0.]))
读写字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2
# {'x': tensor([0, 1, 2, 3, 4]), 'y': tensor([0., 0., 0., 0.])}
定义模型,实例化,并初始化
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)))
net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)
保存模型参数
torch.save(net.state_dict(), 'mlp-params')
读取模型参数
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)
# )
由于两个实例具有相同的模型参数,在输⼊相同的X时,两个实例的计算结果应该相同
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]])