1. 层
层是一个将输入数据转换为输出数据的神经网络组件。每个层都会对输入数据进行一定的操作,例如线性变换、非线性激活函数等,以产生输出数据。
torch.nn模块提供了各种预定义的层,如线性层、卷积层、池化层等,
- nn.Linear:线性层
- nn.MaxPool2d:二维池化层
- nn.Conv2d:二维卷积层
- nn.ReLu:激活函数层
也支持基于nn.Module自定义层。
1.1 自定义简单层
import torch
import torch.nn.functional as F
from torch import nnclass CenteredLayer(nn.Module):def __init__(self):super().__init__()def forward(self, X):return X - X.mean()
这个层的功能是对每个输入减去均值,运行示例:
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))> tensor([-2., -1., 0., 1., 2.])
这个层没有定义需要训练的参数,这一类的层往往用于特定的功能转换,例如数据重排、裁剪、归一化等。
1.2 定义带参数的层
使用nn.Parameter来创建需要训练的参数,以线性全连接层为例
- 需要两个参数:权重和偏置
- 需要两个参数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):return torch.matmul(X, self.weight.data) + self.bias.data
实例化MyLinear类实例并用其进行前向传播计算:
linear = MyLinear(5, 3)
linear(torch.rand(2, 5))> tensor([[ 1.9813, -0.1214, 0.1627],[ 2.6518, -0.8198, 0.6513]])
2. 块
在神经网络中,块可以表示为多个层组成的组件,将多个块组合能构成复杂的网络模型。
从编程的角度(以pytorch为例),块也是由继承nn.Module的类来表示,它必须具有的组成部分:
- 组成块的层
- 前向传播函数forward,用于将输入转换为输出;
- 反向传播函数backward,用于计算梯度;
- 待训练的参数;
由于pytorch支持反向传播自动求导,已经由pytorch内部封装了反向传播函数的实现,另外pytorch会自动根据层的大小来初始化模型参数w和b,所以我们在定义块时只需要考虑前向传播函数和组成块的层。
2.1 自定义块
以前一篇文章中的多层感知机为例,可以封装为一个块:
class MLP(nn.Module):# 用模型参数声明层。这里,我们声明两个全连接的层def __init__(self):# 调用MLP的父类Module的构造函数来执行必要的初始化。# 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)super().__init__()self.hidden = nn.Linear(20, 256) # 隐藏层self.out = nn.Linear(256, 10) # 输出层# 定义模型的前向传播,即如何根据输入X返回所需的模型输出def forward(self, X):# 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。return self.out(F.relu(self.hidden(X)))
2.2 组合块
前篇文章用到的nn.Sequential其实也是一个块,只不过它的作用是将其它块按顺序组合到一起,形成一个串行执行的有序列表。
class MySequential(nn.Module):def __init__(self, *args):super().__init__()for idx, module in enumerate(args):# module是Module子类的一个实例,这里将它保存在'Module'类型的成员变量_modules中,_modules的类型是OrderedDictself._modules[str(idx)] = moduledef forward(self, X):# OrderedDict保证了按照成员添加的顺序遍历它们for block in self._modules.values():X = block(X)return X
每个nn.Module都有一个内置的_modules属性,目的是方便系统查找需要初始化参数的子块
3. 参数管理
训练模型的目的是为了找到使损失函数最小化的模型参数值,这个训练过程就如同我们调试程序一样,有时候需要打印中间结果以辅助我们进行问题的分析和诊断,所以我们有必要知道如何访问参数。
3.1 参数访问
当通过Sequential类定义模型时, 我们可以通过索引来访问模型的任意层,通过每层的state_dict()来获取该层的参数。
print(net[2].state_dict())
OrderedDict([('weight', tensor([[-0.0427, -0.2939, -0.1894, 0.0220, -0.1709, -0.1522, -0.0334, -0.2263]])), ('bias', tensor([0.0887]))])
可以看出,该层包含权重weight和偏置bias两个参数。
我们还可以直接访问权重或偏置。
print(type(net[2].weight)) # 类型
print(net[2].weight) # 直接访问参数,包含参数值和梯度信息
print(net[2].weight.data) # 访问参数值
# print(net[2].weight.grid) # 访问参数的梯度,还没有训练,所以梯度还没值
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([[ 0.0986, 0.2894, 0.3461, 0.2734, -0.3395, -0.0719, -0.3348, -0.0305]],requires_grad=True)
tensor([[ 0.0986, 0.2894, 0.3461, 0.2734, -0.3395, -0.0719, -0.3348, -0.0305]])
3.2 嵌套块参数访问
我们可以将多个块相互嵌套,组成更大的块。
- block1有4层, linear, relu, linear, relu
- block2嵌套了3个block1块
- 最后将block2与一个线性输出层组合,构成一个网络
def block1():return nn.Sequential(nn.Linear(4, 2), nn.ReLU(),nn.Linear(2, 4), nn.ReLU())def block2():net = nn.Sequential()# block2中嵌套3个block1for i in range(3):net.add_module(f'block {i}', block1())return netrgnet = nn.Sequential(block2(), nn.Linear(4, 1))
print(rgnet)
这个包含嵌套块的网络结构如下:
Sequential((0): Sequential((block 0): Sequential((0): Linear(in_features=4, out_features=2, bias=True)(1): ReLU()(2): Linear(in_features=2, out_features=4, bias=True)(3): ReLU())(block 1): Sequential((0): Linear(in_features=4, out_features=2, bias=True)(1): ReLU()(2): Linear(in_features=2, out_features=4, bias=True)(3): ReLU())(block 2): Sequential((0): Linear(in_features=4, out_features=2, bias=True)(1): ReLU()(2): Linear(in_features=2, 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.4917, -0.3920])
3.3 参数初始化
对于参数的初始化,不明确指定时,pytorch会使用默认的随机初始化方法。PyTorch的nn.init模块也提供了多种可供选择的预置初始化方法。
- 指定使用正态分布的随机变量来初始化:
# 定义初始化函数
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.0101, -0.0117, -0.0116, -0.0016]), 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.))
- 对不同的块使用不同的初始化:
net[0].apply(init_normal)
net[2].apply(init_constant)
print(net[0].weight.data[0])
print(net[2].weight.data)> tensor([ 0.0054, -0.0188, -0.0112, 0.0097])
tensor([[1., 1., 1., 1., 1., 1., 1., 1.]])
4.4 参数绑定
含义:通过将一个层共享,可以实现相同的参数权重用于神经网络中的多个层。目的在于两方面:
- 减少模型的参数量:通过共享参数,可以大大减少需要学习的参数数量,从而减小模型的复杂度。
- 加速训练:参数共享可以减少内存占用和计算量,特别是在具有大量参数的深层网络中,可以显著提高计算效率。
下面是一个参数共享的代码示例:
# 给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
# 第二层和第四层共享shared层的参数
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),shared, nn.ReLU(),shared, nn.ReLU(),nn.Linear(8, 1))
输出第二层和第四层指定位置的初始参数,两者是相同的。
print(net[2].weight.data[0, 0])
print(net[4].weight.data[0, 0])> tensor(-0.0253)
tensor(-0.0253)
修改第二层的参数:
net[2].weight.data[0, 0] = 100
再次输出第二层和第四层指定位置的参数,两者都变成了修改后的参数:
print(net[2].weight.data[0, 0])
print(net[4].weight.data[0, 0])> tensor(100.)
tensor(100.)