前期回顾:
Pytorch学习笔记(1):基本概念、安装、张量操作、逻辑回归
Pytorch学习笔记(2):数据读取机制(DataLoader与Dataset)
Pytorch学习笔记(3):图像的预处理(transforms)
一、网络模型的创建步骤
网络创建流程:
模型构建的两个要素:
- 构建子模块:在自己建立的模型(继承nn.Module)的_init_()方法
- 拼接子模块:是在模型的forward()方法中
以LeNet模型为例:
在init函数中构建子模块,构建网络需要的卷积层、池化层、激活函数等
def __init__(self, classes): super(LeNet, self).__init__() self.conv1 = nn.Conv2d(3, 6, 5) self.conv2 = nn.Conv2d(6, 16, 5) self.fc1 = nn.Linear(16*5*5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, classes)
在forward函数中拼接子模块 ,就是模型的实现
def forward(self, x): out = F.relu(self.conv1(x)) out = F.max_pool2d(out, 2) out = F.relu(self.conv2(out)) out = F.max_pool2d(out, 2) out = out.view(out.size(0), -1) out = F.relu(self.fc1(out)) out = F.relu(self.fc2(out)) out = self.fc3(out) return out
二、nn.Mudule的属性
在模型的概念当中,有一个非常重要的概念叫做nn.Module, 我们所有的模型,所有的网络层都是继承于这个类的。
• torch.nn: 这是Pytorch的神经网络模块, 这里的Module就是它的子模块之一,另外还有几个与Module并列的子模块, 这些子模块协同工作,各司其职。
在nn.Module中,有8个重要的属性, 用于管理整个模型,他们都是以有序字典的形式存在着:
• _parameters: 存储管理属于nn.Parameter类的属性,例如权值,偏置这些参数
• _modules: 存储管理nn.Module类, 比如LeNet中,会构建子模块,卷积层,池化层,就会存储在_modules中
• _buffers:存储管理缓冲属性, 如BN层中的running_mean, std等都会存在这里面
• ***_hook:存储管理钩子函数(5个与hooks有关的字典,这个先不用管)
nn.Module属性构建:
在nn.Module类中进行属性赋值时,被setattr函数拦截,在该函数中,判断即将要赋值的这个数据类型是否是nn.Parameter类,是则存储到parameters这个字典中;如果是nn.Module类,则存储在module这个字典中进行管理
nn.module总结:
- 一个module可以包含多个子module(LeNet包含卷积层,池化层,全连接层)
- 一个module相当于一个运算, 必须实现forward函数(从计算图的角度去理解)
- 每个module都有8个字典管理它的属性(最常用的就是_parameters,_modules)
具体代码段如下:
class Model(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 20, 5) self.conv2 = nn.Conv2d(20, 20, 5) def forward(self, x): x = F.relu(self.conv1(x)) return F.relu(self.conv2(x))
三、模型容器Containers
3.1 nn.Sequential
nn.Sequential 是 nn.module的容器,用于按顺序包装一组网络层
以LeNet为例,将卷积池化放到一个sequential中进行特征提取,将全连接层放到一个sequential中进行分类,然后将这两个sequential拼接起来,就是LeNet网络
(1)输入数据类型非字典
用sequential容器构建包装子模型:
'''------------Sequential---------------''' class LeNetSequential(nn.Module): def __init__(self, classes): super(LeNetSequential, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 6, 5), nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(6, 16, 5), nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2),) self.classifier = nn.Sequential( nn.Linear(16*5*5, 120), nn.ReLU(), nn.Linear(120, 84), nn.ReLU(), nn.Linear(84, classes),) def forward(self, x): x = self.features(x) x = x.view(x.size()[0], -1) x = self.classifier(x) return x
可以看到,LeNet在这里分成了两大部分:
- 第一部分是features模块,用于特征提取
- 第二部分是classifier部分,用于分类
每一部分都是各种网络的堆叠,然后用sequential包装起来。 然后它的forward函数也比较简单, 只需要features处理输出,然后形状变换,然后classifier就完成了。
进入sequential类的init函数,调用init函数构建相应的属性,得到八个有序字典。
首先判断输入的参数的数据类型是否为有序字典,如果为有序字典,则可以对网络层进行命名操作,如果输入不为字典,则执行else操作
在for循环中将网络层添加到sequential中
def __init__(self, *args: Any): super(Sequential, self).__init__() if len(args) == 1 and isinstance(args[0], OrderedDict): for key, module in args[0].items(): self.add_module(key, module) else: for idx, module in enumerate(args): self.add_module(str(idx), module)
调试中可以看出,每个网络层都被加入到module中,但此时网络层的对应的索引都为序号。
打印网络层,我们可以看到:
从中我们可以看出,每个网络层都是用序号来索引的,如果网络层过多,则很难通过序号去索引网络层,故可以对网络层进行命名。
(2)输入数据类型为字典
对sequential输入一个有序的字典来对网络层进行命名:
class LeNetSequentialOrderDict(nn.Module): def __init__(self, classes): super(LeNetSequentialOrderDict, self).__init__() self.features = nn.Sequential(OrderedDict({ 'conv1': nn.Conv2d(3, 6, 5), 'relu1': nn.ReLU(inplace=True), 'pool1': nn.MaxPool2d(kernel_size=2, stride=2), 'conv2': nn.Conv2d(6, 16, 5), 'relu2': nn.ReLU(inplace=True), 'pool2': nn.MaxPool2d(kernel_size=2, stride=2), })) self.classifier = nn.Sequential(OrderedDict({ 'fc1': nn.Linear(16*5*5, 120), 'relu3': nn.ReLU(), 'fc2': nn.Linear(120, 84), 'relu4': nn.ReLU(inplace=True), 'fc3': nn.Linear(84, classes), })) def forward(self, x): x = self.features(x) x = x.view(x.size()[0], -1) x = self.classifier(x) return x
这里面Sequential包装的就是一个有序的字典, 字典中是网络名:网络层
的形式。通过这个就可以对每一层网络进行命名
def __init__(self, *args: Any): super(Sequential, self).__init__() if len(args) == 1 and isinstance(args[0], OrderedDict): for key, module in args[0].items(): self.add_module(key, module) else: for idx, module in enumerate(args): self.add_module(str(idx), module)
调试结果中可以看出,网络层被写入sequential,注意,每个网络层前面都有单独的命名。不同于前面的序号索引:
打印网络层,我们可以看到,不同于之前,每个网络层都有自己的名字,可以更方便的通过网络名来索引网络:
总结——sequential
nn.sequential是nn.module的容器,用于按顺序包装一组网络层
- 顺序性:各网络层之间严格按照顺序构建
- 自带forward():自带的forward里,通过for循环依次执行前向传播运算
3.2 nn.ModuleList
nn.ModuleList是nn.module的容器,用于包装一组网络层,以迭代方式调用网络层
主要方法:
- append():在ModuleList后面添加网络层
- extend():拼接两个ModuleList
- insert():指定在ModuleList中位置插入网络层
例:我们使用ModuleList来循环迭代的实现一个20个全连接层的网络的构建。
'''-------- ModuleList----------''' class ModuleList(nn.Module): def __init__(self): super(ModuleList, self).__init__() self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(20)]) def forward(self, x): for i, linear in enumerate(self.linears): x = linear(x) return x net = ModuleList() print(net) fake_data = torch.ones((10, 10)) output = net(fake_data) print(output)
进入container的modulelist类
将网络层加入到module中
def __init__(self, modules: Optional[Iterable[Module]] = None) -> None: super(ModuleList, self).__init__() if modules is not None: self += modules
打印网络层,我们可以看到:
3.3 nn.ModuleDict
nn.ModuleDict是nn.module的容器,用于包装一组网络层,以索引方式调用网络层。
主要方法:
- clear():清空ModuleDict
- items():返回可迭代的键值对(key-value pairs)
- keys():返回字典的键(key)
- values():返回字典的值(values)
- pop():返回一对键值,并从字典中删除
具体代码如下:
'''-----------ModuleDict---------------''' class ModuleDict(nn.Module): def __init__(self): super(ModuleDict, self).__init__() self.choices = nn.ModuleDict({ 'conv': nn.Conv2d(10, 10, 3), 'pool': nn.MaxPool2d(3) }) self.activations = nn.ModuleDict({ 'relu': nn.ReLU(), 'prelu': nn.PReLU() }) def forward(self, x, choice, act): x = self.choices[choice](x) x = self.activations[act](x) return x net = ModuleDict() fake_img = torch.randn((4, 10, 32, 32)) output = net(fake_img, 'conv', 'relu') # 在这里可以选择我们的层进行组合 print(output)
上面通过self.choices这个ModuleDict可以选择卷积或者池化
下面通过self.activations这个ModuleDict可以选取是用哪个激活函数
这在选择网络层的时候挺实用,比如要做时间序列预测的时候,我们往往会用到GRU或者LSTM, 我们就可以通过这种方式来对比哪种网络的效果好。 而具体选择哪一层是前向传播那完成,会看到多了两个参数。
打印网络层,我们可以看到:
容器总结
nn.sequential:顺序性,各网络层之间严格按照顺序执行,常用于block构建
nn.ModuleList:迭代性,常用于大量重复网络构建,通过for循环实现重复构建
nn.ModuleDict:索引性,常用于可选择的网络层
四、 AlexNet构建
关于AlexNet的网络结构详解可参考我这篇文章:经典神经网络论文超详细解读(一)——AlexNet学习笔记(翻译+精读)
下面看看AlexNet的源代码:
class AlexNet(nn.Module): def __init__(self, num_classes=1000): super(AlexNet, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(64, 192, kernel_size=5, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), nn.Conv2d(192, 384, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(256, 256, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), ) self.avgpool = nn.AdaptiveAvgPool2d((6, 6)) self.classifier = nn.Sequential( nn.Dropout(), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplace=True), nn.Dropout(), nn.Linear(4096, 4096), nn.ReLU(inplace=True), nn.Linear(4096, num_classes), ) def forward(self, x): x = self.features(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = self.classifier(x) return x
本文参考: