原创 Pytorch教程(十七):实现最简单的CNN

在这里插入图片描述
上面定义了一个简单地神经网络CNN,它包含了两个卷积层,三个全连接层(又叫先行层或者Dense层),我们的每一层都扩展了pytorch的神经网络模块类,并且在每一层中有两个东西默认封装在里面,一个forward前向传播方法和一个权重张量,每层中的权重张量包含了随着我们的网络在训练过程中学习而更新的权重值,这就是我们在我们的Network类中指定的层作为类属性的原因。

在Linear层中,我们使用了权重张量,下面是Linear的源码:
在这里插入图片描述
这个权重张量将被我们的Network类继承,并且作为我们网络可学习的参数,在Module.linear中的定义:self.register_parameter(name, value)就将我们的权重张量进行了注册,我们将学习在层构造函数中传递给这些参数的参数和值。

在我们构建一个layer时,我们将每个layer的参数传递给layer构造函数,在卷积层中我们有三个参数in_channels, out_channels, kernel_size,在线性层中有两个参数in_features, out_features。所以对于卷积层我们有输入通道、输出通道、卷积核大小;对于线性层我们有输入特征、输出特征。

我们看看这些参数值如何确定。

卷积核大小设置了该层中使用的滤波器的大小,在深度学习中,“内核”这个词也表示“滤波器Filter”,我们可以说“卷积核”也可以说“卷积滤波器”,在一个卷积层中,输入通道与一个卷积滤波器配对来执行卷积运算。卷积核可以包含输入通道,这个操作的结果是一个输出通道,所以一个包含输入通道的卷积核可以给我们一个输入通道,这就是为什么当我们设置输出通道时,我们实际上是在设置卷积核的数量,例如当out_channles=6表示我们想要6个输出通道,我们在该层有6个卷积核。

在上面的例子中,我们第一个卷积层中,我们有一个输入通道将会被6个不同的卷积核处理,这一过程将创建6个输出通道,这些输出通道也有另一个名字特征图(Feature maps)

如果我们处理的是线性层,我们一般不称他们为特征映射,因为输出是的是一个一阶张量,我们把它叫做特征,所以我们有输出特征,而不是输出通道或者特征图。

我们可以观察出,在增加一个卷积层时,我们增加了输出通道,而在我们切换到线性层时,我们缩小了数据的特征。

现在,我们看看上面这些参数哪些依赖于数据?
依赖的超参数在网络的开始和末端,即第一个卷积层的输入通道和最后一个线性层的输出通道。第一个卷积层的输入通道依赖于构成训练集的图像内部的彩色通道的数量,因为我们处理的是灰度图像,所以这个值为1;输出层的输出特征依赖与我们的训练集中的分类的数量,因为我们在Fashion-MNIST数据集里有十种数量,所以我们有10个输出特征,这10个输出是来自网络对每个分类的预测。一层的输入是上一层的输出,而当我们将卷积层传递到线性层时,我们需要将张量拍平flat,因此这里的输入维度是 12 × 4 × 4 × 4 12 \times 4 \times4 \times 4 12×4×4×4,12来自于前一层的输出通道的数量,那为什么有两个4呢?因为4*4表示的是feature map的大小。

可学习的参数是指在训练过程中学习的参数,对于可学习的参数,我们通常从一组任意的初始化值开始,当网络学习时,这些值就会以迭代的方式进行更新。事实上,当我们说一个网络正在学习,我们的意思是网络正在学习这些参数的“适当的值”,而“适当的值”是最小化损失函数的值。

我们的网络架构:
在这里插入图片描述
上图列出了我们的网络层,并显示了传递给层的构造函数的值。

这些是怎么显示的呢?我们知道我们的Network类是从pytorch 的Module基类继承这个功能的,如果我们停止扩展神经网络的Module类会发生什么:
在这里插入图片描述
可以看出输出没有像之前那样的结果,相反却得到了一个类似于指针的东西。这个字符串来自python的默认基类Object,所有的Python类都会自动扩展Object类。当我们想输出我们期待的输出是,我们需要通过override一个方法,来改变他的原始实现方式,这个方式就是override __repr__()方法,这个方法名是represent的缩写:
在这里插入图片描述
通过override repr方法之后,我们在打印我们的network时,就显示出了我们定义的输出。我们在pytorch的Module源代码中也可以看出,它对__repr__方法做了override:
在这里插入图片描述

我们从Network的输出结果来看:
在这里插入图片描述
虽然conv1传入的参数是kernel_size=5,但是它的打印结果确是kernel_size=(5, 5),这时因为我们的filter实际上有一个高度和宽度,当我们把一个数字传递给构造函数时,Layer构造函数中的代码假设我们想要一个正方形的filter。接下来有一个stride=(1, 1),但是我们并没有在Layer构造函数中指定stride,但是Layer会自动设置,这个stride的作用就是告诉卷积层在整个卷积过程中,每个操作之后,filter应该滑动的距离,stride=(1, 1)表示向右移动一个单位,且向下移动一个单位。对于线性层,我们有一个额外的参数叫偏差(bias),它的默认值是true,可以通过设置为False来关闭。

通过对象访问属性。

在这里插入图片描述
在这里插入图片描述
我们通过network.conv1.weight可以访问得到它的权重值,观察到最上面的一行输出Parameter containing,这个因为这个tensor是一个特定的tensor,它的值确实学习了我们的网络的所有参数,这意味着我们在这里看到的张量中的值,实际上是通过网络的训练来进行学习的,当训练的时候,这些权重值会以某种方式(如梯度下降)进行更新,使得损失函数最小,为了追踪网络中所有的权重张量,pytorch有一个名为parameter的特殊类:
在这里插入图片描述

parameter类继承了tensor类,所以每一层的权重张量就是这个parameter类的一个实例,这就是为什么我们会在张量输出的顶部看到parameter containing,我们再仔细看,parameter类是覆盖了repr方法。

神经网络module类基本上是在寻找一个模块的任意属性,其值是parameter类的实例,当他找到一个属性时,他将跟踪他。
在这里插入图片描述
目前为止,所有的这些事情都是在幕后进行的细节,我们需要掌握的是这些权重张量的shape:
在这里插入图片描述
在conv中,第一个轴代表filter的数量,第二个轴代表filter的深度,它对应于所涉及的输入通道的数量,最后两个轴代表每个filter的高度和宽度。network.conv2.weight[0].shape返回[6,5,5],这返回给我们一个单独的filter,他的深度是6,高度和宽度是5。

如何一次性访问所有的parameter?
下面这时最常用的方式,当我们在训练过程中更新权重时,我们将使用这个来迭代我们的权重,我们的网络从神经网络module基类中继承了这个参数方法。
在这里插入图片描述
下面这些方法是向我们展示我们如何能看到这些名字,我们可以看到bias也是一个可学习的参数,默认情况下每一层都有偏差,所以对于每一层我们有一个权重张量和一个偏置张量。
在这里插入图片描述

forward 向前传播

我们首先回顾一下上面的Network网络:

import torch.nn as nn
import torch.nn.functional as F

class Network(nn.Module):
    def __init__(self):
        super(Network, self).__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 fowward(self, t):
        return t

我们刚刚通过继承构建nn.Module来创建了一个CNN网络,然后在网络的构造函数中我没让你将网络的层定义为类属性,现在我们需要实现向前传播,然后就可以训练网络了。

我们知道我们的家forward方法接受一个向量作为输入,然后返回一个向量作为输出,目前为止我们返回的向量就是输入向量。我们将使用上面定义的网络层来实现我们的forward:

    def forward(self, t):
        # (1) input layer
        t = t
        
        # (2)hidden conv layer
        t = self.conv1(t)
        t = F.relu(t)
        t = F.max_pool2d(t, kernel_size=2, stride=2)
        
        # (3) hidden conv layer
        t = self.conv2(t)
        t = F.relu(t)
        t = F.max_pool2d(t, kernel_size=2, stride=2)
        
        # (4) hidden linear layer
        t = t.reshape(-1, 12 * 4 * 4)  # 4*4 是12个channel的高度和宽度
        t = self.fc1(t)
        t = F.relu(t)
        
        # (5) hidden linear layer
        t = self.fc2(t)
        t = F.relu(t)
        
        # (6) output layer
        t = self.out(t)
        # 这里我们不适用softmax,我们将从nn模块中使用交叉熵损失函数,它在其输入上隐式的执行一个softmax操作,所以这里我们只返回最后一个线性变换过程,这意味着我们的网络将使用softmax操作进行训练
        # t = F.softmax(t, dim=1)
        return t

简单实现一个向前传播

class Network(nn.Module):
    def __init__(self):
        super(Network, self).__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):
        # (1) input layer
        t = t
        
        # (2)hidden conv layer
        t = self.conv1(t)
        t = F.relu(t)
        t = F.max_pool2d(t, kernel_size=2, stride=2)
        
        # (3) hidden conv layer
        t = self.conv2(t)
        t = F.relu(t)
        t = F.max_pool2d(t, kernel_size=2, stride=2)
        
        # (4) hidden linear layer
        t = t.reshape(-1, 12 * 4 * 4)  # 4*4 是12个channel的高度和宽度
        t = self.fc1(t)
        t = F.relu(t)
        
        # (5) hidden linear layer
        t = self.fc2(t)
        t = F.relu(t)
        
        # (6) output layer
        t = self.out(t)
        # 这里我们不适用softmax,我们将从nn模块中使用交叉熵损失函数,它在其输入上隐式的执行一个softmax操作,所以这里我们只返回最后一个线性变换过程
        # t = F.softmax(t, dim=1)
        return t

使用单一的数据样本进行计算:

torch.set_grad_enabled(False) # 将特征关闭
network = Network()
sample = next(iter(train_set))

image,label = sample
print(image.shape)  # torch.Size([1, 28, 28])
#image.unsqueeze(0).shape #  添加一个维度,表示批次

pred = network(image.unsqueeze(0))
print(pred)  # tensor([[-0.0452, -0.0204,  0.1011, -0.0511,  0.0731, -0.0873, -0.0268, -0.0545, -0.0813,  0.0869]])
print(pred.shape) # torch.Size([1, 10])
print(pred.argmax(dim=1)) # tensor([2]) 
print(label) # 9
F.softmax(pred, dim=1) # 让pred值变为概率 tensor([[0.0964, 0.0988, 0.1116, 0.0958, 0.1085, 0.0924, 0.0982, 0.0955, 0.0930, 0.1100]])

使用批处理进行计算:

data_loader = torch.utils.data.DataLoader(
    train_set,
    batch_size=10
)
batch = next(iter(data_loader))  # batch size, input channels, height, width
images, labels = batch
print(images.shape)
print(labels.shape)
preds = network(images)
print(preds.shape) # torch.Size([10, 10])
print(preds.argmax(dim=1)) # tensor([2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
print(labels) # tensor([False, False, False, False, False,  True, False,  True, False, False])

def get_num_correct(preds, labels):
    return pred.argmax(dim=1).eq(labels).sum().item()
get_num_correct(preds, labels) # 2

Guess you like

Origin blog.csdn.net/vincent_duan/article/details/120814637