PyTorch - 19 - 可调用神经网络 - 深度的线性层
How Linear Layers Work
在本系列的最后一篇文章中,我们学习了线性层如何使用矩阵乘法将其内部特征转换为外部特征。
当输入特征被线性层接收时,它们将以平坦的一维张量形式接收,然后与权重矩阵相乘。 该矩阵乘法产生输出特征。
让我们在代码中看一个例子。
Transform Using A Matrix
in_features = torch.tensor([1,2,3,4], dtype=torch.float32)
weight_matrix = torch.tensor([
[1,2,3,4],
[2,3,4,5],
[3,4,5,6]
], dtype=torch.float32)
> weight_matrix.matmul(in_features)
tensor([30., 40., 50.])
在这里,我们创建了一个称为in_features的一维张量。 我们还创建了一个权重矩阵,它当然是二维张量。 然后,我们使用matmul()函数来执行产生一维张量的矩阵乘法运算。
通常,权重矩阵定义一个线性函数,该函数将具有四个元素的一维张量映射到具有三个元素的一维张量。 我们可以将此函数视为从4维欧几里得空间到3维欧几里得空间的映射。
这也是线性层如何工作的方式。 他们使用权重矩阵将in_feature空间映射到out_feature空间。
Transform Using A PyTorch Linear Layer
让我们看看如何创建一个PyTorch线性图层来执行相同的操作。
fc = nn.Linear(in_features=4, out_features=3, bias=False)
在这里,我们有它。我们定义了一个线性图层,该图层接受4个输入要素并将其转换为3个输出要素,因此我们从4维空间转换为3维空间。我们知道,权重矩阵用于执行此操作,但是此示例中的权重矩阵在哪里?
我们将权重矩阵存在于PyTorch LinearLayer类中,并由PyTorch创建。 PyTorch LinearLayer类使用传递给构造函数的数字4和3创建3 x 4权重矩阵。让我们通过查看PyTorch源代码来验证这一点。
# torch/nn/modules/linear.py (version 1.0.1)
def __init__(self, in_features, out_features, bias=True):
super(Linear, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.weight = Parameter(torch.Tensor(out_features, in_features))
if bias:
self.bias = Parameter(torch.Tensor(out_features))
else:
self.register_parameter('bias', None)
self.reset_parameters()
如我们所见,当我们将3 x 4矩阵与4 x 1矩阵相乘时,结果是3 x 1矩阵。这就是为什么PyTorch以此方式构建权重矩阵的原因。这些是用于矩阵乘法的线性代数规则。
让我们看看如何通过传递in_features张量来调用层。
> fc(in_features)
tensor([-0.8877, 1.4250, 0.8370], grad_fn=<SqueezeBackward3>)
我们可以这样称呼对象实例,因为PyTorch神经网络模块是可调用的Python对象。我们将在一分钟内更仔细地研究这个重要的细节,但首先,请查看此输出。实际上,我们确实获得了包含三个元素的一维张量。但是,产生了不同的值。
这是因为PyTorch会创建一个权重矩阵,并使用随机值对其进行初始化。这意味着两个示例中的线性函数是不同的,因此我们使用不同的函数来生成这些输出。
请记住,权重矩阵内的值定义了线性函数。这说明了在训练过程中,网络的映射如何随着权重的更新而变化。
让我们显式设置线性层的权重矩阵,使其与我们在其他示例中使用的权重矩阵相同。
fc.weight = nn.Parameter(weight_matrix)
PyTorch模块权重需要作为参数。这就是为什么我们将权重矩阵张量包装在参数类实例中的原因。现在让我们看一下该层如何使用新的权重矩阵转换输入。我们希望看到与前面的示例相同的结果。
> fc(in_features)
tensor([30.0261, 40.1404, 49.7643], grad_fn=<AddBackward0>)
这次我们更接近30、40和50的值。但是,我们是正确的。为什么是这样?我们会说的不对,因为线性层在输出上添加了一个偏置张量。观察当我们关闭偏差时会发生什么。为此,我们将False标志传递给构造函数。
fc = nn.Linear(in_features=4, out_features=3, bias=False)
fc.weight = nn.Parameter(weight_matrix)
> fc(in_features)
tensor([30., 40., 50.], grad_fn=<SqueezeBackward3>)
在那里,现在我们有一个完全匹配。这就是线性层的工作方式。
Mathematical Notation Of The Linear Transformation
有时我们会看到称为:
y
=
A
x
+
b
y=Ax + b
y=Ax+b
在此等式中,我们具有以下内容:
变量 | 定义 |
---|---|
A | 权重矩阵张量 |
x | 输入张量 |
b | 偏张量 |
y | 输出张量 |
我们将注意到,这类似于直线的方程式
y
=
m
x
+
b
y=mx + b
y=mx+b
Callable Layers And Neural Networks
我们之前指出了将图层对象实例称为函数是多么奇怪。
> fc(in_features)
tensor([30.0261, 40.1404, 49.7643], grad_fn=<AddBackward0>)
使之成为可能的是PyTorch模块类实现了另一个特殊的Python函数__call __()。如果一个类实现__call __()方法,则在调用对象实例时将调用特殊的调用方法。
这个事实是一个重要的PyTorch概念,因为__call __()方法与我们的图层和网络的forward()方法交互的方式。
代替直接调用forward()方法,我们调用对象实例。调用对象实例后,将在幕后调用__call __()方法,然后__call __()依次调用forward()方法。这适用于所有PyTorch神经网络模块,即网络和层。
让我们在PyTorch源代码中看到这一点。
# torch/nn/modules/module.py (version 1.0.1)
def __call__(self, *input, **kwargs):
for hook in self._forward_pre_hooks.values():
hook(self, input)
if torch._C._get_tracing_state():
result = self._slow_forward(*input, **kwargs)
else:
result = self.forward(*input, **kwargs)
for hook in self._forward_hooks.values():
hook_result = hook(self, input, result)
if hook_result is not None:
raise RuntimeError(
"forward hooks should never return any values, but '{}'"
"didn't return None".format(hook))
if len(self._backward_hooks) > 0:
var = result
while not isinstance(var, torch.Tensor):
if isinstance(var, dict):
var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
else:
var = var[0]
grad_fn = var.grad_fn
if grad_fn is not None:
for hook in self._backward_hooks.values():
PyTorch在__call __()方法中运行的额外代码是为什么我们从不直接调用forward()方法的原因。如果这样做,将不会执行其他PyTorch代码。结果,任何时候我们想要调用forward()方法时,我们都将调用对象实例。这适用于层和网络,因为它们都是PyTorch神经网络模块。
现在,我们准备实现网络的forward()方法。下一个见!