在PyTorch中,一个简单的神经网络的例子可以在https://visualstudiomagazine.com/articles/2020/10/14/pytorch-define-network.aspx上找到。
class Net(T.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.hid1 = T.nn.Linear(4, 8) # 4-(8-8)-1
self.hid2 = T.nn.Linear(8, 8)
self.oupt = T.nn.Linear(8, 1)
T.nn.init.xavier_uniform_(self.hid1.weight)
T.nn.init.zeros_(self.hid1.bias)
T.nn.init.xavier_uniform_(self.hid2.weight)
T.nn.init.zeros_(self.hid2.bias)
T.nn.init.xavier_uniform_(self.oupt.weight)
T.nn.init.zeros_(self.oupt.bias)
def forward(self, x):
z = T.tanh(self.hid1(x))
z = T.tanh(self.hid2(z))
z = T.sigmoid(self.oupt(z))
return z
上面的一个显著特征是,层被存储为网络对象中的字段(正如它们所需要的那样,因为它们包含了需要跨培训阶段记住的权重),但是激活函子(如tanh
)是在每次调用forward
时重新创建的。作者说:
二进制分类网络最常见的结构是在
__init__()
方法中定义网络层及其相关的权重和偏差,并在forward()
方法中定义输入输出计算。
当然可以。另一方面,也许存储函子比在每次调用forward
时重新创建函子要快得多。第三,它不太可能产生任何可测量的差异,这意味着它可能最终是一个代码风格的问题。
这是最常见的做法吗?无论哪种方式都有任何技术优势,还是仅仅是风格问题?
发布于 2022-07-04 09:59:27
关于“存储”函子
代码段没有“重新创建”任何内容--调用torch.tanh(x)
实际上只是调用由带有参数的torch
包导出的函数x
。
其他方法
我认为这个片段是一个小的神经块的一个公平的例子,是使用和忘记,或只是不打算参数化。根据您的意图,当然还有其他的选择,但是您必须权衡增加的复杂性是否具有任何价值。
允许从固定集合中选择激活函数
class Model(torch.nn.Module):
def __init__(..., activation_function: Literal['tanh'] | Literal['relu']):
...
if activation_function == 'tanh':
self.activation_function = torch.tanh
elif activation_function == 'relu':
self.activation_function = torch.relu
else:
raise ValueError(f'activation function {activation_function} not allowed, use tanh or relu.'}
def forward(...) -> Tensor:
output = ...
return self.activation_function(output)
使用任意模块或函数作为激活
class Model(torch.nn.Module):
def __init__(..., activation_function: torch.nn.Module | Callable[[Tensor], Tensor]):
self.activation_function = activation_function
def forward(...) -> Tensor:
output = ...
return self.activation_function(output)
例如,它的工作方式如下
def cube(x: Tensor) -> Tensor: return x**3
cubic_model = Model(..., activation_function=cube)
上述示例与代码片段之间的关键区别在于,后者是透明的和可调整的wrt。对于所使用的激活,您可以检查激活函数(即model.activation_function
),并对其进行更改(在初始化之前或之后),而对于原始片段,它是不可见的,并被放入模型的功能中(为了用不同的函数复制模型,您需要从头开始定义它)。
总的来说,我认为最好的方法是创建小的、本地可调的块,这些块尽可能地是参数化的,并将它们封装到更大的块中,对包含的参数进行概括。也就是说,如果您的大模型由5个线性层组成,您可以为一个层(包括退出、层规范等)制作一个单一的激活参数包装器,然后为N层流制作另一个包装器,它要求使用哪一个激活函数来初始化其子层。换句话说,当您预料到这将使您避免额外的工作和将来复制粘贴代码时,请进行泛化和参数化,但不要做过头,否则您最终会远离您原来的规范和需求。
ps:我不知道调用激活函数函子是否合理。
https://stackoverflow.com/questions/72850229
复制相似问题