类的构造(深度学习基础语法)
在搭建神经网络的时候,常会遇到class Network(nn.Module):这样的类定义。实际上,这就是我们定义的子类Network对父类nn.Module的继承。
继承的作用就是省去对nn.Module已定义过的函数进行定义,让网络搭建敲代码更简单,代码更简洁。如下图所示,就是代码中网络的搭建。(大致看看即可,这一部分只为介绍,重点在下一节)
class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.feature_encoder = D_Res_3d_CNN(1, 8, 16)
self.final_feat_dim = FEATURE_DIM
self.target_mapping = Mapping(TAR_INPUT_DIMENSION, N_DIMENSION)
self.source_mapping = Mapping(SRC_INPUT_DIMENSION, N_DIMENSION)
self.binary_classifier = BinaryClassifier(160, 100)
def forward(self, x, domain='source'):
if domain == 'target':
x = self.target_mapping(x)
elif domain == 'source':
x = self.source_mapping(x)
feature = self.feature_encoder(x)
binary_predictions = self.binary_classifier(feature)
return feature, binary_predictions
我们从上往下观看代码。
1. 类的定义。(我们将对类的编程叫做“面向对象编程”,常见于C语言教学)
class Network(nn.Module):
在以上代码中,我们定义了名叫Network的类,括号中是Network继承的父类。
2. 方法的定义。(在类中定义的函数,通常叫做“类的方法”,也可以叫做函数)
def __init__(self):
在以上代码中,我们定义了类的初始化方法。有同学可能很疑惑:为什么init函数左右有下滑线。事实上,这是初始化的语法,类似于关键字,记住就行。
每次调用类之后会自动调用初始化方法。
3. super().函数
super(Network, self).__init__()
调用父类的初始化方法。括号中的Network, self是Python2中固定的语法成分(子类名称和self),在Python3中也可以使用。而Python3,有更简洁的语法,如下
super().__init__()
4. 方法定义
self.feature_encoder = D_Res_3d_CNN(1, 8, 16)
self.final_feat_dim = FEATURE_DIM
self.target_mapping = Mapping(TAR_INPUT_DIMENSION, N_DIMENSION)
self.source_mapping = Mapping(SRC_INPUT_DIMENSION, N_DIMENSION)
self.binary_classifier = BinaryClassifier(160, 100)
在类的内部,我们定义了这些方法,赋值号右边都是我已经定义过的函数。函数左边,是后续搭建网络需要用到的函数。
5. 网络搭建
def forward(self, x, domain='source'):
if domain == 'target':
x = self.target_mapping(x)
elif domain == 'source':
x = self.source_mapping(x)
feature = self.feature_encoder(x)
binary_predictions = self.binary_classifier(feature)
return feature, binary_predictions
在定义的前向传播函数中,搭建了由target_mapping或者source_mapping(根据数据情况不同进入不同的层)、feature_encoder和binary_classifier三层组成的前向传播网络。其中的每层网络结构,都是在上面的初始化方法中被定义过了。定义的方式就是赋值,如下所示:
def __init__(self):
super(Network, self).__init__()
self.feature_encoder = D_Res_3d_CNN(1, 8, 16)
self.final_feat_dim = FEATURE_DIM
self.target_mapping = Mapping(TAR_INPUT_DIMENSION, N_DIMENSION)
self.source_mapping = Mapping(SRC_INPUT_DIMENSION, N_DIMENSION)
self.binary_classifier = BinaryClassifier(160, 100)
通过赋值语句,定义了4种网络结构(分别是特征编码器feature_encoder对应3d残差卷积层D_Res_3d_CNN,目标域映射层和源域映射层target_mapping 对应不同输入输出参数的映射层Mapping,二进制分类器binary_classifier对应二进制分类器BinaryClassifier ),后面的赋值都是我先前定义过的。
✳有同学可能疑惑:为什么需要 super().函数?如下:
super().__init__()
在这份代码中,在逻辑上确实不需要这个函数(但实际上并不可以这样,下面逐渐解释原因),因为我没有使用父类中的任何方法。
但是如下的类就不同了。
class D_Res_3d_CNN(nn.Module): # 使用残差块的3dCNN
def __init__(self, in_channel, out_channel1, out_channel2):
super(D_Res_3d_CNN, self).__init__()
self.block1 = residual_block(in_channel, out_channel1)
self.maxpool1 = nn.MaxPool3d(kernel_size=(4, 2, 2), padding=(0, 1, 1), stride=(4, 2, 2))
self.block2 = residual_block(out_channel1, out_channel2)
self.maxpool2 = nn.MaxPool3d(kernel_size=(4, 2, 2), stride=(4, 2, 2), padding=(2, 1, 1))
self.conv = nn.Conv3d(in_channels=out_channel2, out_channels=32, kernel_size=3, bias=False)
self.final_feat_dim = 160
def forward(self, x): # x:(400,100,9,9)
x = x.unsqueeze(1) # (400,1,100,9,9)
x = self.block1(x) # (1,8,100,9,9)
x = self.maxpool1(x) # (1,8,25,5,5)
x = self.block2(x) # (1,16,25,5,5)
x = self.maxpool2(x) # (1,16,7,3,3)
x = self.conv(x) # (1,32,5,1,1)
x = x.view(x.shape[0], -1) # (1,160)
return x
可以看到在D_Res_3d_CNN类中多次使用了来自父类的网络层搭建网络。
✳那么有同学可能会问:能不能进行类内部参数的传递,在搭建网络的时候直接调用nn库呢?这样似乎可以化简代码。比如上面的一大堆代码可以改写成下面的。
class D_Res_3d_CNN(nn.Module): # 使用残差块的3dCNN
def __init__(self, in_channel, out_channel1, out_channel2):
self.in_channel = in_channel
self.out_channel1 = out_channel1
self.out_channel2 = out_channel2
def forward(self, x): # x:(400,100,9,9)
in_channel = self.in_channel
out_channel1 = self.out_channel1
out_channel2 = self.out_channel2
x = x.unsqueeze(1) # (400,1,100,9,9)
x = residual_block(in_channel, out_channel1) # (1,8,100,9,9)
x = nn.MaxPool3d(kernel_size=(4, 2, 2), padding=(0, 1, 1), stride=(4, 2, 2)) # (1,8,25,5,5)
x = residual_block(out_channel1, out_channel2) # (1,16,25,5,5)
x = nn.MaxPool3d(kernel_size=(4, 2, 2), stride=(4, 2, 2), padding=(2, 1, 1)) # (1,16,7,3,3)
x = self.conv(x) # (1,32,5,1,1)
x = x.view(x.shape[0], -1) # (1,160)
return x
这样写逻辑上似乎也是可以的,但是运行代码就会有如下报错:AttributeError: cannot assign module before Module.__init__() call。所以我们默认以前的形式,并且加上super(D_Res_3d_CNN, self).__init__()就好。(如果不加上super还是会有相同的报错,详细的解释在“✳✳✳”下面的部分,原因是在深度学习进行网络定义参数用赋值语句传递之前,还有应该有别的操作。)
✳✳✳底层原因:我们不能使用等号进行“函数的传递”。
def pd(x):
print(x)
Datapd = pd()
Datapd(1)
class FFF:
def pd(x):
print(x)
Datapd = FFF.pd()
Datapd(1)
可以看到在上面的两个例子中,我想让Datapd实现pd函数的功能,我直接用赋值进行函数的传递。这都是不行的。它们是相同的报错信息:TypeError: FFF.pd() missing 1 required positional argument: 'x'
在类中我们可以通过函数封装的方法给函数重命名函数。下例中子类fff中顺利“传递”了函数。
class FFF:
def pd(x):
print(x)
class fff(FFF):
def Datapd(self):
FFF.pd(self)
fff.Datapd(1)
但仍然不能使用等号进行函数的传递。
class FFF:
def pd(x):
print(x)
class fff(FFF):
def __init__(self):
self.Datapd = FFF.pd(self)
def foward(self):
self.Datapd(self)
fff.foward(1)
运行代码有报错信息:AttributeError: 'int' object has no attribute 'Datapd'
所以super(D_Res_3d_CNN, self).__init__()这行代码,正是实现了使得self.Datapd = FFF.pd(self)这种以赋值语句传递函数的功能可以成功运行的代码块。所以在搭建传播网络结构的时候才可以直接使用初始化过程中通过赋值语句“传递函数”的类内部的self.函数
x = self.block1(x) # (1,8,100,9,9)
x = self.maxpool1(x) # (1,8,25,5,5)
x = self.block2(x) # (1,16,25,5,5)
x = self.maxpool2(x) # (1,16,7,3,3)
x = self.conv(x) # (1,32,5,1,1)
类的实例化
这一部分我参考了这个博文:python中super().__init__()_super.init-CSDN博客 写的非常详细。重复的部分我不再赘述。
我补充一部分:
我们猜想如下的代码是否可以成功输出 hello A
class A:
def hi(self):
print('hi A')
def hello(self):
print('hello A')
class B(A):
def hi(self):
super().hi()
b = B()
b.hello()
是可以输出hello A的。原因:虽然在B类中没有明确定义hello函数,但由于子类B对父类A的继承,所以仍然可以使用父类中的同名函数hello()
类继承的这种功能,在实际工程应用广泛,如上文中的代码及其实例化:
class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.feature_encoder = D_Res_3d_CNN(1, 8, 16)
self.final_feat_dim = FEATURE_DIM
self.target_mapping = Mapping(TAR_INPUT_DIMENSION, N_DIMENSION)
self.source_mapping = Mapping(SRC_INPUT_DIMENSION, N_DIMENSION)
self.binary_classifier = BinaryClassifier(160, 100)
def forward(self, x, domain='source'):
if domain == 'target':
x = self.target_mapping(x)
elif domain == 'source':
x = self.source_mapping(x)
feature = self.feature_encoder(x)
binary_predictions = self.binary_classifier(feature)
return feature, binary_predictions
for i_experiment in range(n_experiment):
# 网络实例化
net = Network()
# 初始化权值
net.apply(weights_init)
# 加载到gpu
net.cuda()
# 模型设置成训练模式
net.train()
可以看到在网络中没有定义apply() cuda() train()等方法,但是仍然可以通过对父类nn.Module的继承,使用其中的同名函数。
✳那么问题来了,在不做初始化的时候super()有什么作用?能不能直接不对B进行定义,完全使用A?(虽然在工程上没有实际价值)
class A:
def hi(self):
print('hi A')
def hello(self):
print('hello A')
class B(A):
def hi(self):
b=B()
b.hi()
我直接将super().hi()删掉。在Python3.10解释器中报错了,不能在函数的定义后不写东西。
最终改写成如下,用pass直接写在函数内部。就实现了对于A的完全继承。
class A:
def hi(self):
print('hi A')
def hello(self):
print('hello A')
class B(A):
pass
b=B()
b.hi()
如果如下的话是不行的
class A:
def hi(self):
print('hi A')
def hello(self):
print('hello A')
class B(A):
def hi(self):
pass
b=B()
b.hi()
这样会出现不打印出任何东西的BUG
所以,不做初始化的时候super()的作用是仍然是让敲代码更简单,代码更简洁。
更改深度学习中的网络结构
如上文中的代码:
class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.feature_encoder = D_Res_3d_CNN(1, 8, 16)
self.final_feat_dim = FEATURE_DIM
self.target_mapping = Mapping(TAR_INPUT_DIMENSION, N_DIMENSION)
self.source_mapping = Mapping(SRC_INPUT_DIMENSION, N_DIMENSION)
self.binary_classifier = BinaryClassifier(160, 100)
def forward(self, x, domain='source'):
if domain == 'target':
x = self.target_mapping(x)
elif domain == 'source':
x = self.source_mapping(x)
feature = self.feature_encoder(x)
binary_predictions = self.binary_classifier(feature)
return feature, binary_predictions
for i_experiment in range(n_experiment):
# 网络实例化
net = Network()
# 初始化权值
net.apply(weights_init)
# 加载到gpu
net.cuda()
# 模型设置成训练模式
net.train()
我们只需要对于前向传播函数进行更改,即可实现网络结构的修改。
总结
1. 当我们使用类的继承,再重新定义函数的目的是为了在调用父类方法的基础上重新写新的方法。前文中super().hi()的作用就是更加集成化地调用了父类的方法,直接把代码块调用了。
2. 关于类的初始化,虽然通过一些代码的变化可以省略,但往往都是不会省略的。类的初始化完成了将外部参数传入类的内部的作用,以self形式传递在类的内部。
3. 类的初始化在类实例化的时候自动被调用。可以不进行类的初始化,也就是不往类里面传递任何外界参数,只把类当作一个“函数工厂”来使用,但是不能在搭建类的时候忘记写self(就蛮无语的)。
4. 赋值语句均不能进行函数的传递,只能进行参数的传递。在神经网络中,如果要进行函数的传递,在初始化函数的第一行语句处加上super().__init__()是有必要的。