一、__xxx__形式的魔法方法
我们可以经常在python代码片段中看到类的定义,其中第一个被定义的方法往往是__init__,如下所示:
class Accumulator: """在n个变量上累加""" def __init__(self, n): self.data = [0.0] * n def add(self, *args): self.data = [a + float(b) for a, b in zip(self.data, args)] def reset(self): self.data = [0.0] * len(self.data) def __getitem__(self, idx): return self.data[idx]
我们知道__init__显然是类的构造函数,但为什么要在前后都加上双下划线呢?
原来这是python设计的一种特殊方法,它别称为魔法方法,它可以被运算符隐式调用,下面给出示例:
import torch class Accumulator: #@save """在n个变量上累加""" def __init__(self, n): self.data = [0.0] * n def add(self, *args): self.data = [a + float(b) for a, b in zip(self.data, args)] def reset(self): self.data = [0.0] * len(self.data) def __getitem__(self, idx): return self.data[idx] test=Accumulator(3) test.add(0,1,2) print(test.__getitem__(2)) #打印2.0 print(test[2]) #打印2.0
可以发现,所谓魔法方法,实质上实现的就是c++中运算符重载的功能。 它让test.__getitem__(2)和test[2]这两种语法都能调用该方法!
对于更多的python的魔法方法,可以看看下面这一篇文章:
Python 中的 `__xxx__` 特殊方法:介绍与使用-CSDN博客
其中,与pytorch紧密相关的一个方法比较重要,这便是__call__方法,它能通过以下方式被直接调用:
test=Accumulator(3) test.__call__(param) test(param) '''两者等价'''
示例如下:
import torch class Accumulator: #@save """在n个变量上累加""" def __init__(self, n): self.data = [0.0] * n def add(self, *args): self.data = [a + float(b) for a, b in zip(self.data, args)] def reset(self): self.data = [0.0] * len(self.data) def __call__(self, idx): return self.data[idx] test=Accumulator(3) test.add(0,1,2) print(test.__call__(2)) #打印2.0 print(test(2)) #打印2.0
二、nn.Module类的使用以及其重要的两个类方法
神经网络较为复杂,可以被分为块和层。用类去分别定义神经网络的一个层、一个块会更为清晰。而nn.Module便是pytorch设计者为块类和层类设计的父类,如nn.Linear、nn.Flatten和nn.ReLU这样描述层的类,便是继承自nn.Module类。我们也可以继承nn.Module类,编写我们所需要的层和类。
nn.Module类有一个非常非常重要的特征,那便是其__call__函数里调用了forward方法。所以,在继承nn.Module类时,需要为子类写好forward方法,这样才能按照pytorch的习惯发挥子类作为神经网络中层和类的功能!
例如,我现在写一个类用来描述多层感知机这个块,那么,我需要写好forward和__init__两个方法
import torch from torch import nn from torch.nn import functional as func class MulLayerPerceptron(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(func.relu(self.hidden(X))) net=MulLayerPerceptron() X=torch.arange(20.0) print(net(X))
这样,在实例化这个类的对象net后,就可以直接使用net(X)这个函数调用forward方法!
三、Sequential子类
torch框架本省就提供了一个nn.Module的子类——Sequential,这是一个包含多个层的类,可以按顺序执行一系列函数
如上的多层感知机,我就可以这样定义:
net=nn.Sequential(nn.Linear(20,256),nn.Linear(256,10)) X=torch.arange(20.0) print(net(X))
事实上,其源码的形式如下所示:
class MySequential(nn.Module): def __init__(self,*args): super().__init__() self.layers=[] for layer in args: self.layers.append(layer) def forward(self,X): for layer in self.layers: X=layer(X) return X net=MySequential(nn.Linear(20,256),nn.Linear(256,10)) X=torch.arange(20.0) print(net(X))
当然,实际上Sequential类会更加复杂。
使用对net使用add_module方法可以为为Sequential块类添加层,其中第一个参数是层的名称,第二个参数是层的类型。
net=nn.Sequential(nn.Linear(20,256)) net.add_module("mylayer",nn.Linear(256,10))
直接打印net可以查看整个Sequential块的组成情况:
print(net)
Sequential(
(0): Linear(in_features=20, out_features=256, bias=True)
(mylayer): Linear(in_features=256, out_features=10, bias=True)
)
可以看出,Sequential的层默认按照数字命名,我们添加的层是自主命名的!
四、参数访问
1、简单结构
对于Sequential类,可以使用类似数组下标访问的方法来访问其每一层:
net=nn.Sequential(nn.Linear(20,256),nn.Linear(256,10)) X=torch.arange(20.0) print(net[0]) print(net[1])
Linear(in_features=20, out_features=256, bias=True)
Linear(in_features=256, out_features=10, bias=True)
可以用state_dict方法看看net[X]中的各个参数:
net=nn.Sequential(nn.Linear(20,256),nn.Linear(256,10)) print(net[0].state_dict())
一般,nn.Linear结构中的参数包括weight和bias,即权重和偏置,可以用.运算符访问:
net=nn.Sequential(nn.Linear(20,256),nn.Linear(256,10)) print(net[0].weight) print(net[0].bias)
其中,weight和bias都分别包含tensor数组(可用data属性访问)和控制是否求梯度的bool变量requires_grad,可以如下进行访问和设置
net=nn.Sequential(nn.Linear(20,256),nn.Linear(256,10)) print(net[0].weight.data[0]) net[0].weight.requires_grad=True
事实上,net[X]返回了nn库自定义的Parameter类型。
2、嵌套结构
block=nn.Sequential(nn.Linear(20,256),nn.Linear(256,10)) net=nn.Sequential(block,nn.Linear(10,5)) X=torch.arange(20.0) print(net(X)) print(net) print(net[0][0])
运行结果:
tensor([ 1.9557, -2.0318, -1.2498, -0.1433, -0.3641], grad_fn=<ViewBackward0>)
Sequential(
(0): Sequential(
(0): Linear(in_features=20, out_features=256, bias=True)
(1): Linear(in_features=256, out_features=10, bias=True)
)
(1): Linear(in_features=10, out_features=5, bias=True)
)
Linear(in_features=20, out_features=256, bias=True)
如上所示,Sequential类可以进行嵌套。嵌套后,我们使用类似多维数组的方式访问。
五、参数初始化
对于如nn.Linear这样的层,可以使用nn.init下带有的方法将参数初始化。
def init_normal(m): if type(m)==nn.Linear: nn.init.normal_(m.weight,mean=0.0,std=0.01) nn.init.zeros_(m.bias)
如代码所示,nn.Linear作为参数m传入,那么调用nn.init.normal_可以使weight向量得以初始化。第一个参数使所需要初始化的向量,mean参数规定平均值,std参数规定方差。
nn.init.zeros_方法用于将参数置于0,nn.init.constant_方法可以将参数置为某个常数。
nn.init.zeros_(m.weight) nn.init.constant_(m.weight,1.0) #将weight向量全置为1
而nn.init.uniform_方法,可以使参数均匀分布。其第一个参数还是需要初始化的tensor向量,a代表均匀分布的下界(默认为0.0),b代表上界(默认为1.0)
nn.init.uniform_(m.weight,a=0.0,b=2.0)
对块net使用apply方法,可以让自定义初始化函数在所有层上作用一遍。
block=nn.Sequential(nn.Linear(20,256),nn.Linear(256,10)) net=nn.Sequential(block,nn.Linear(10,5)) X=torch.arange(20.0) def init_normal(m): if type(m)==nn.Linear: print("init") nn.init.normal_(m.weight,mean=0.0,std=0.01) nn.init.zeros_(m.bias) net.apply(init_normal)
init
init
init
注意,apply非常智能。纵使Sequential中出现嵌套,也可以层层访问,把所有的nn.Linear结构都进行初始化。
这是因为apply函数会把net的所有结构都访问一遍,打印m,就可以知晓其运作的规律:
block=nn.Sequential(nn.Linear(20,256),nn.Linear(256,10)) net=nn.Sequential(block,nn.Linear(10,5)) X=torch.arange(20.0) def init_normal(m): print(m) if type(m)==nn.Linear: nn.init.normal_(m.weight,mean=0.0,std=0.01) nn.init.zeros_(m.bias) net.apply(init_normal)
Linear(in_features=20, out_features=256, bias=True)
Linear(in_features=256, out_features=10, bias=True)
Sequential(
(0): Linear(in_features=20, out_features=256, bias=True)
(1): Linear(in_features=256, out_features=10, bias=True)
)
Linear(in_features=10, out_features=5, bias=True)
Sequential(
(0): Sequential(
(0): Linear(in_features=20, out_features=256, bias=True)
(1): Linear(in_features=256, out_features=10, bias=True)
)
(1): Linear(in_features=10, out_features=5, bias=True)
)