PyTorch - 18 - CNN权重 - PyTorch神经网络中的可学习参数

Our Neural Network

在本系列的最后几篇文章中,我们开始构建CNN,并进行了一些工作以了解我们在网络的构造函数中定义的层。

class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)

        self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
        self.fc2 = nn.Linear(in_features=120, out_features=60)
        self.out = nn.Linear(in_features=60, out_features=10)

    def forward(self, t):
        # implement the forward pass
        return t

最终,我们整个流程的下一步是在网络的前进方法中使用这些层,但是现在,让我们看一下网络中可学习的参数。

我们已经知道超参数。我们看到超参数是参数,其值是任意选择的。

到目前为止,我们使用的超参数是我们用来构造网络体系结构的参数,尽管我们将这些层构造并分配为类属性。

超参数值是任意选择的。

这些超参数并不是唯一的超参数,并且在开始训练过程时,我们将看到更多的超参数。我们现在关心的是我们网络的可学习参数。

Learnable Parameters

可学习的参数是其值在训练过程中被学习的参数。

有了可学习的参数,我们通常从一组任意值开始,然后随着网络学习以迭代方式更新这些值。

实际上,当我们说网络正在学习时,我们具体是指网络正在学习适用于可学习参数的适当值。 适当的值是使损失函数最小的值。

当涉及到我们的网络时,我们可能会思考,这些可学习的参数在哪里?

可学习的参数在哪里?

我们将学习的参数是网络内部的权重,它们存在于每一层中。

Getting An Instance The Network

在PyTorch中,我们可以直接检查重量。让我们获取我们的网络类的一个实例,看看吧。

network = Network()

请记住,要获取我们的Network类的对象实例,请键入类名,后跟括号。执行此代码后,将运行__init__类构造函数中的代码,并在返回对象实例之前将图层分配为属性。

名称__init__是初始化的缩写。在对象的情况下,属性使用值初始化,并且这些值的确可以是其他对象。这样,对象可以嵌套在其他对象内。

我们的网络类就是这种情况,其网络类属性是使用PyTorch图层类的实例初始化的。初始化对象后,我们可以使用网络变量访问对象。

在开始使用新创建的网络对象之前,请查看将网络传递给Python的print()函数时会发生什么。

> print(network)
Network(
    (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
    (fc1): Linear(in_features=192, out_features=120, bias=True)
    (fc2): Linear(in_features=120, out_features=60, bias=True)
    (out): Linear(in_features=60, out_features=10, bias=True)
)

print()函数将控制台的字符串表示形式输出到控制台。凭着敏锐的眼光,我们可以注意到这里的打印输出详细说明了网络的体系结构,列出了网络的层,并显示了传递给层构造函数的值。

Network String Representation

不过,有一个问题。 这是怎么回事?

这个字符串表示法从哪里来?

我们的网络类将从PyTorch Module基类继承此功能。 观察如果我们停止扩展神经网络模块类会发生什么。

> print(network)
<__main__.Network object at 0x0000017802302FD0>

现在,我们没有像以前那样好的描述性输出。 相反,我们得到了这种技术上的胡言乱语,这是我们不提供的默认Python字符串表示形式。

因此,在面向对象的编程中,我们通常希望在类中提供对象的字符串表示形式,以便在打印对象时获得有用的信息。 这种字符串表示形式来自Python的默认基类object(对象)。

How Overriding Works

所有Python类都会自动扩展对象类。如果我们想为我们的对象提供一个自定义的字符串表示形式,我们可以做到,但是我们需要引入另一个面向对象的概念,称为覆盖。

当我们扩展一个类时,我们获得了它的所有功能,作为补充,我们可以添加其他功能。但是,我们也可以通过将现有功能更改为不同的行为来覆盖现有功能。

我们可以使用__repr__函数覆盖Python的默认字符串表示形式。该名称是表示的简称。

def __repr__(self):
    return "lizardnet"

这次,当我们将网络传递给打印函数时,将在类定义中指定的字符串打印出来,以代替Python的默认字符串。

> print(network)
lizardnet

当我们之前讨论OOP时,我们了解了__init__方法以及它是一种用于构造对象的特殊Python方法。

我们还会遇到其他特殊方法,__ repr__是其中一种。所有特殊的OOP Python方法通常在修复前后都有双下划线。

这也是PyTorch Module基类的工作方式。 Module基类重写__repr__函数。

What’s In The String Representation?

在大多数情况下,PyTorch给我们提供的字符串表示形式与我们根据配置网络层的方式所期望的大致匹配。

但是,还有一些其他信息我们应该重点介绍。

Convolutional Layers

对于卷积层,即使我们仅在构造函数中传递数字5,kernel_size参数还是一个Python元组(5,5)。

Network(
    (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
    (fc1): Linear(in_features=192, out_features=120, bias=True)
    (fc2): Linear(in_features=120, out_features=60, bias=True)
    (out): Linear(in_features=60, out_features=10, bias=True)
)

这是因为我们的过滤器实际上具有高度和宽度,并且当我们传递一个数字时,该图层的构造函数中的代码假定我们需要一个正方形过滤器。

self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)

步幅是我们可以设置的附加参数,但我们省略了它。如果在图层构造函数中未指定跨度,则图层将自动对其进行设置。

步幅告诉conv层,在每次卷积后,滤波器在整个卷积中应滑动多远。该元组说向右移动时单位滑动,而向下移动时单位滑动。

Linear Layers

对于线性层,我们有一个称为bias的附加参数,其默认参数值为true。 可以通过将其设置为false来关闭它。

self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
self.fc2 = nn.Linear(in_features=120, out_features=60)
self.out = nn.Linear(in_features=60, out_features=10) 

关于在打印对象时为对象显示的信息,需要注意的一件事是,它完全是任意信息。

作为开发人员,我们可以决定在此处放置任何信息。 但是,Python文档告诉我们,该信息应足够完整,以便在需要时可用于重构对象。

Accessing The Network’s Layers

好了,现在我们有了网络的实例,并且已经检查了图层,让我们看看如何在代码中访问它们。

在Python和许多其他编程语言中,我们使用点表示法访问对象的属性和方法。

> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))

> network.conv2
Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))

> network.fc1
Linear(in_features=192, out_features=120, bias=True)

> network.fc2                                    
Linear(in_features=120, out_features=60, bias=True)

> network.out
Linear(in_features=60, out_features=10, bias=True)

这是点表示法。使用点表示法时,我们使用点来表示我们想要打开对象并访问其中的东西。我们已经使用了很多,所以这里提到的只是给我们一个概念标签。

关于这一点,需要注意的一点与我们刚才在谈论网络的字符串表示形式时直接相关的是,这些代码中的每段代码也都为我们提供了每一层的字符串表示形式。

对于网络而言,网络类实际上只是将所有这些数据汇总在一起,从而为我们提供单个输出。

Network(
    (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
    (fc1): Linear(in_features=192, out_features=120, bias=True)
    (fc2): Linear(in_features=120, out_features=60, bias=True)
    (out): Linear(in_features=60, out_features=10, bias=True)
)

关于这些对象的字符串表示形式,最后要提到的一件事是,在这种情况下,我们实际上并没有使用print方法。

之所以仍然返回字符串表示形式,是因为我们正在使用Jupyter笔记本,并且在后台,笔记本正在访问字符串表示形式,因此它可以向我们展示一些内容。这就像是字符串表示形式主要用例的一个很好的例子。

Accessing The Layer Weights

现在我们可以访问每个图层,可以访问每个图层中的权重了。让我们来看一下我们的第一卷积层。

> network.conv1.weight
Parameter containing:
tensor([[[[ 0.0692,  0.1029, -0.1793,  0.0495,  0.0619],
            [ 0.1860,  0.0503, -0.1270, -0.1240, -0.0872],
            [-0.1924, -0.0684, -0.0028,  0.1031, -0.1053],
            [-0.0607,  0.1332,  0.0191,  0.1069, -0.0977],
            [ 0.0095, -0.1570,  0.1730,  0.0674, -0.1589]]],

        [[[-0.1392,  0.1141, -0.0658,  0.1015,  0.0060],
            [-0.0519,  0.0341,  0.1161,  0.1492, -0.0370],
            [ 0.1077,  0.1146,  0.0707,  0.0927,  0.0192],
            [-0.0656,  0.0929, -0.1735,  0.1019, -0.0546],
            [ 0.0647, -0.0521, -0.0687,  0.1053, -0.0613]]],

        [[[-0.1066, -0.0885,  0.1483, -0.0563,  0.0517],
            [ 0.0266,  0.0752, -0.1901, -0.0931, -0.0657],
            [ 0.0502, -0.0652,  0.0523, -0.0789, -0.0471],
            [-0.0800,  0.1297, -0.0205,  0.0450, -0.1029],
            [-0.1542,  0.1634, -0.0448,  0.0998, -0.1385]]],

        [[[-0.0943,  0.0256,  0.1632, -0.0361, -0.0557],
            [ 0.1083, -0.1647,  0.0846, -0.0163,  0.0068],
            [-0.1241,  0.1761,  0.1914,  0.1492,  0.1270],
            [ 0.1583,  0.0905,  0.1406,  0.1439,  0.1804],
            [-0.1651,  0.1374,  0.0018,  0.0846, -0.1203]]],

        [[[ 0.1786, -0.0800, -0.0995,  0.1690, -0.0529],
            [ 0.0685,  0.1399,  0.0270,  0.1684,  0.1544],
            [ 0.1581, -0.0099, -0.0796,  0.0823, -0.1598],
            [ 0.1534, -0.1373, -0.0740, -0.0897,  0.1325],
            [ 0.1487, -0.0583, -0.0900,  0.1606,  0.0140]]],

        [[[ 0.0919,  0.0575,  0.0830, -0.1042, -0.1347],
            [-0.1615,  0.0451,  0.1563, -0.0577, -0.1096],
            [-0.0667, -0.1979,  0.0458,  0.1971, -0.1380],
            [-0.1279,  0.1753, -0.1063,  0.1230, -0.0475],
            [-0.0608, -0.0046, -0.0043, -0.1543,  0.1919]]]], 
            requires_grad=True
)

输出是张量,但是在看张量之前,让我们先讨论一下OOP。这是一个很好的示例,展示了如何嵌套对象。我们首先访问位于网络对象内部的转换层对象。

network.conv1.weight

然后,我们访问位于conv层对象内部的权重张量对象,以便将所有这些对象链接或链接在一起。

在这里插入图片描述

关于权重张量输出要注意的一件事是它说参数包含在输出的顶部。这是因为这个特定的张量是一个特殊的张量,因为它的值或标量分量是我们网络可学习的参数。

这意味着该张量内的值(我们在上面看到的值)实际上是在训练网络时获悉的。在我们训练时,这些权重值会以使损失函数最小化的方式进行更新。

PyTorch Parameter Class

跟踪网络中所有的权重张量。 PyTorch有一个称为Parameter的特殊类。 Parameter类扩展了张量类,因此每层内部的权重张量就是此Parameter类的实例。这就是为什么我们在字符串表示输出的顶部看到包含文本的Parameter的原因。

我们可以在Pytorch源代码中看到Parameter类通过将包含在常规张量类表示输出中的text参数添加到__repr__函数中来覆盖__repr__函数。

def __repr__(self):
    return 'Parameter containing:\n' + super(Parameter, self).__repr__()

PyTorch的nn.Module类基本上是在寻找其值是Parameter类的实例的任何属性,并且当它找到参数类的实例时,就会对其进行跟踪。

所有这些实际上都是在幕后进行的PyTorch技术细节,我们将看到其中的一部分。

现在就我们的理解而言,重要的部分是重量张量的形状的解释。在这里,我们将开始使用在本系列早期学习的关于张量的知识。

现在让我们看一下形状,然后对其进行解释。

Weight Tensor Shape

在上一篇文章中,我们说过传递给图层的参数值会直接影响网络的权重。在这里将看到这种影响。

对于卷积层,权重值位于过滤器内部,而在代码中,过滤器实际上是权重张量本身。

在这里插入图片描述

层内的卷积运算是该层的输入通道与该层内的滤波器之间的运算。这意味着我们真正拥有的是两个张量之间的运算。

话虽如此,让我们解释这些权重张量,这将使我们能够更好地了解网络内部的卷积操作。

请记住,张量的形状实际上编码了我们需要了解的有关张量的所有信息。

对于第一个转换层,我们有1个颜色通道,应由6个5x5大小的滤镜进行卷积以产生6个输出通道。这就是我们解释图层构造函数中的值的方式。

> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))

不过,在图层内部,对于这6个过滤器,我们没有明确地拥有6个权重张量。实际上,我们使用单个权重张量表示所有6个滤波器,其形状反映或说明了6个滤波器。

第一个卷积层的权重张量的形状告诉我们,我们有一个4级权重张量。第一个轴的长度为6,这说明了6个过滤器。

> network.conv1.weight.shape
torch.Size([6, 1, 5, 5])

第二个轴的长度为1,代表单个输入通道,最后两个轴的长度和宽度代表过滤器的高度和宽度。

考虑这一点的方式就像我们将所有过滤器打包到一个张量中一样。

现在,第二个conv层具有12个过滤器,而不是卷积单个输入通道,而是有6个来自上一层的输入通道。

> network.conv2.weight.shape
torch.Size([12, 6, 5, 5])

在这里将此值6赋予每个过滤器一定的深度。我们的过滤器具有的深度与通道数匹配,而不是让过滤器迭代地对所有通道进行卷积。

关于这些卷积层的两个主要要点是,我们的滤波器使用单个张量表示,并且在张量内的每个滤波器也具有解释正在卷积的输入通道的深度。

  1. 所有过滤器均使用单个张量表示。
  2. 过滤器的深度说明输入通道。

我们的张量是4级张量。第一个轴代表过滤器的数量。第二个轴代表每个滤波器的深度,它对应于卷积的输入通道数。

最后两个轴代表每个过滤器的高度和宽度。我们可以通过索引重量张量的第一轴来拉出任何单个过滤器。

(过滤器数量,深度,高度,宽度)

这为我们提供了一个高度和宽度分别为5和深度为6的滤镜。

Weight Matrix

对于线性层或完全连接的层,我们将第1级张量展平为输入和输出。 我们将线性层中的in_features转换为out_features的方式是使用通常称为权重矩阵的rank-2张量。

这是由于重量张量在高度轴和宽度轴上为等级2的事实。

> network.fc1.shape
torch.Size([120, 192])

> network.fc2.shape                                    
torch.Size([60, 120])

> network.out.shape
torch.Size([10, 60])

在这里我们可以看到我们每个线性层都有一个等级2的权重张量。 我们在这里可以看到的模式是权重张量的高度具有所需输出特征的长度和输入特征的宽度。

Matrix Multiplication

这个事实是由于如何执行矩阵乘法。让我们通过一个较小的示例来了解这一点。
假设我们有两个2级张量。第一个形状为3x4,第二个形状为4x1。现在,由于我们要演示的是矩阵乘法,因此请注意,这两个2级张量的确是矩阵。

在这里插入图片描述

对于输出中的每个行-列组合,通过获取第一矩阵的相应行与第二矩阵的相应列的点积来获得该值。

由于本示例中的第二个矩阵仅具有1列,因此我们将其全部使用了3次,但是这种想法是通用的。

该操作起作用的规则是,第一个矩阵中的列数必须与第二个矩阵中的行数匹配。如果该规则成立,则可以执行这样的矩阵乘法运算。

点积意味着我们将相应组件的乘积相加。如果您想知道,点积和矩阵乘法都是线性代数概念。

Linear Function Represented Using A Matrix

像这样的矩阵乘法的重要之处在于它们代表了线性函数,我们可以使用它们来构建神经网络。

具体而言,权重矩阵是线性函数,也称为线性映射,该线性映射将4维的向量空间映射到3维的向量空间。

当我们更改矩阵内的权重值时,实际上是在更改此函数,而这正是我们在搜索网络最终近似的函数时要执行的操作。

让我们看看如何使用PyTorch执行相同的计算。

Using PyTorch For Matrix Multiplication

在这里,我们使用in_features和weight_matrix作为张量,并且我们使用称为matmul()的张量方法执行操作。 我们现在知道的名称matmul()是矩阵乘法的缩写。

> weight_matrix.matmul(in_features)
tensor([30., 40., 50.])

一个迫在眉睫的问题是,我们如何才能一次访问所有参数? 有一个简单的方法。 让我告诉你。

Accessing The Networks Parameters

第一个示例是最常见的方法,我们将在训练过程中更新权重时使用它来遍历权重。

for param in network.parameters():
    print(param.shape)

torch.Size([6, 1, 5, 5])
torch.Size([6])
torch.Size([12, 6, 5, 5])
torch.Size([12])
torch.Size([120, 192])
torch.Size([120])
torch.Size([60, 120])
torch.Size([60])
torch.Size([10, 60])
torch.Size([10])

第二种方法只是显示我们如何也可以看到该名称。这揭示了我们将不详细介绍的内容,偏差也是可学习的参数。默认情况下,每个层都有一个偏差,因此对于每个层,我们都有一个权重张量和一个偏差张量。

for name, param in network.named_parameters():
    print(name, '\t\t', param.shape)

conv1.weight 	 torch.Size([6, 1, 5, 5])
conv1.bias 		 torch.Size([6])
conv2.weight 	 torch.Size([12, 6, 5, 5])
conv2.bias 		 torch.Size([12])
fc1.weight 		 torch.Size([120, 192])
fc1.bias 		 torch.Size([120])
fc2.weight 		 torch.Size([60, 120])
fc2.bias 		 torch.Size([60])
out.weight 		 torch.Size([10, 60])
out.bias 		 torch.Size([10])

Wrapping Up

现在,我们应该对可学习的参数,网络内部的位置以及如何使用PyTorch访问权重张量有了很好的了解。

在下一篇文章中,我们将了解如何通过将张量传递给层来处理它们。我会在那里见你。

猜你喜欢

转载自blog.csdn.net/weixin_48367136/article/details/112530131