PyTorch深度学习实战(11)—— 常用神经网络层

本节对常用的神经网络层进行介绍,这部分内容在神经网络的构建中将发挥重要作用。

1.图像相关层

图像相关层主要包括卷积层(Conv)、池化层(Pool)等,这些层在实际使用中可以分为一维(1D)、二维(2D)和三维(3D)几种情况。池化方式包括平均池化(AvgPool)、最大值池化(MaxPool)、自适应平均池化(AdaptiveAvgPool)等。卷积层除了常用的前向卷积,还有逆卷积或转置卷积(TransposeConv)。

1.1 卷积层

与图像处理相关的网络结构中最重要的就是卷积层。卷积神经网络的本质是卷积层、池化层、激活层以及其他层的叠加,理解卷积层的工作原理是极其重要的。本节以最常见的二维卷积为例对卷积层进行说明。

torch.nn工具箱中,已经封装好了二维卷积类 :

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias, padding_mode)  它有以下六个重要的参数。

  • in_channels:输入图像的维度。常见RGB彩色图像维度为3,灰度图像的维度为1。
  • out_channels:经过卷积操作后输出的维度。
  • kernel_size:卷积核大小,常见卷积核为2维方阵,维度为[T... T],正方形卷积核可以写为Tint。
  • stride:每次卷积操作移动的步长。
  • padding:卷积操作在边界是否有填充,默认为0。
  • bias:是否有偏置,偏置是一个可学习参数,默认为True。

在卷积操作中需要知道输出结果的形状,以便对后续网络结构进行设计。假设输入的形状为Hin, Win,输出的形状为Hout, Wout ,通过下面的式子可以得到卷积输出结果的形状。

下面举例说明卷积操作的具体过程:

In: from PIL import Image
    from torchvision.transforms import ToTensor, ToPILImage
    to_tensor = ToTensor() # img → Tensor
    to_pil = ToPILImage()
    lena = Image.open('imgs/lena.png')
    lena # 将lena可视化输出

In: # 输入是一个batch,batch_size=1
    lena = to_tensor(lena).unsqueeze(0) 
    print("Input Size:",lena.size()) # 查看input维度
    # 锐化卷积核
    kernel = t.ones(3, 3) / (-9.)
    kernel[1][1] = 1
    conv = nn.Conv2d(1, 1, (3, 3), 1, bias=False)
    conv.weight.data = kernel.view(1, 1, 3, 3)
    
    out = conv(lena)
    print("Output Size:",out.size())
    to_pil(out.data.squeeze(0))
 
 Out:Input Size: torch.Size([1, 1, 200, 200])
    Output Size: torch.Size([1, 1, 198, 198])

在上面的例子中,输入Tensor的大小为200×200,卷积核大小为3×3,步长为1,填充为0,根据式(4.1)可以计算得到输出的形状为:Hout=Wout = \lfloor {200+{2}\times{0}-3 \over 1 }+1\rfloor=198$,这与程序输出的维度一致。

本小节以二维卷积为例,对卷积层的输入输出进行了介绍。除了二维卷积,图像的卷积操作还有各种变体,感兴趣的读者可以进一步查阅相关资料。

1.2 池化层

池化层可以看作是一种特殊的卷积层,它主要用于下采样。增加池化层可以在保留主要特征的同时降低参数量,从而一定程度上防止过拟合。池化层没有可学习参数,它的weight是固定的。在torch.nn工具箱中封装好了各种池化层,常用的有最大池化和平均池化,下面举例说明:

In: input = t.randint(10, (1, 1, 4, 4))
    print(input)
    pool = nn.AvgPool2d(2, 2) # 平均池化,池化中的卷积核为2×2,步长默认等于卷积核长度,无填充
    pool(input)Out:tensor([[[[6, 8, 9, 2],
            [0, 3, 1, 4],
            [7, 0, 9, 9],
            [9, 3, 2, 7]]]])Out:tensor([[[[4, 4],
            [4, 6]]]])In: list(pool.parameters()) # 可以看到,池化层中并没有可学习参数。Out:[]In: out = pool(lena)
    to_pil(out.data.squeeze(0)) # 输出池化后的lena

1.3 其他层

除了卷积层和池化层,深度学习中还经常使用以下几个层。

  • Linear:全连接层。
  • BatchNorm:批标准化层,分为1D、2D和3D。除了标准的BatchNorm,还有在风格迁移中常用到的InstanceNorm层。
  • Dropout:Dropout层用于防止过拟合,同样分为1D、2D和3D。

下面举例说明它们的使用方法:

In: # 输入的batch_size为2,维度为3
    input = t.randn(2, 3)
    linear = nn.Linear(3, 4)
    h = linear(input)
    h
 Out:tensor([[-0.2782, -0.7852,  0.0166, -0.1817],
            [-0.1064, -0.5069, -0.2169, -0.0372]], grad_fn=<AddmmBackward>)
 
 In: # 4 channel,初始化标准差为4,均值为0
    bn = nn.BatchNorm1d(4)
    bn.weight.data = t.ones(4) * 4
    bn.bias.data = t.zeros(4)
    
    bn_out = bn(h)
    bn_out
 Out:tensor([[-3.9973, -3.9990,  3.9985, -3.9962],
            [ 3.9973,  3.9990, -3.9985,  3.9962]],
            grad_fn=<NativeBatchNormBackward>)
 
 In: # 注意输出的均值和方差
    bn_out.mean(0), bn_out.std(0, unbiased=False)
 Out:(tensor([ 0.0000e+00, -8.3447e-07,  0.0000e+00,  0.0000e+00],
            grad_fn=<MeanBackward1>),tensor([3.9973, 3.9990, 3.9985, 3.9962], grad_fn=<StdBackward1>))

In: # 每个元素以0.5的概率随机舍弃
    dropout = nn.Dropout(0.5)
    o = dropout(bn_out)
    o # 有一半左右的数变为0
Out:tensor([[-7.9946, -0.0000,  0.0000, -0.0000], [ 0.0000,  0.0000, -7.9971,  7.9923]], grad_fn=<MulBackward0>)

以上例子都是对module的可学习参数直接进行操作,在实际使用中,这些参数一般会随着学习的进行不断改变。除非需要使用特殊的初始化,否则应该尽量不要直接修改这些参数。

2. 激活函数

线性模型不能够解决所有的问题,因此激活函数应运而生。激活函数给模型加入非线性因素,可以提高神经网络对模型的表达能力,解决线性模型不能解决的问题。PyTorch实现了常见的激活函数,它们可以作为独立的layer使用,这些激活函数的具体接口信息可以参考官方文档。这里对最常用的激活函数ReLU进行介绍,它的数学表达式如式(4.2)所示。

下面举例说明如何在torch.nn中使用ReLU函数:

In: relu = nn.ReLU(inplace=True)
    input = t.randn(2, 3)
    print(input)
    output = relu(input)
    print(output) # 小于0的都被截断为0
    # 等价于input.clamp(min=0)
Out:tensor([[ 0.1584,  1.3065,  0.6037],
            [ 0.4320, -0.0310,  0.0563]])
    tensor([[0.1584, 1.3065, 0.6037],
            [0.4320, 0.0000, 0.0563]])

ReLU函数有个inplace参数,如果设为True,那么它的输出会直接覆盖输入,可以有效节省内存/显存。之所以这里可以直接覆盖输入,是因为在计算ReLU的反向传播时,只需要根据输出就能够推算出反向传播的梯度。只有少数的autograd操作才支持inplace操作,如tensor.sigmoid_()。如果一个Tensor只作为激活层的输入使用,那么这个激活层可以设为inplace=True。除了ReLU函数,常见的激活函数还有tanh函数和sigmoid函数,读者可以根据实际的网络结构、数据分布等灵活地选用各类激活函数。

3. 构建神经网络

在上面的例子中,每一层的输出基本上都直接成为下一层的输入,这样的网络称为前馈传播网络(Feedforward Neural Network,FFN)。对于此类网络,每次都编写复杂的forward函数会比较麻烦,这里有两种简化方式:ModuleList和Sequential。Sequential是一个特殊的module,它可以包含几个子module,前向传播时会将输入一层接一层地传递下去。ModuleList也是一个特殊的module,它也可以包含几个子module,读者可以像使用list一样使用它,但不能直接将输入传给ModuleList。下面举例说明:

In: # Sequential的三种写法
    net1 = nn.Sequential()
    net1.add_module('conv', nn.Conv2d(3, 3, 3))
    net1.add_module('batchnorm', nn.BatchNorm2d(3))
    net1.add_module('activation_layer', nn.ReLU())
    
    net2 = nn.Sequential(
                nn.Conv2d(3, 3, 3),
                nn.BatchNorm2d(3),
                nn.ReLU()
            )
    
    from collections import OrderedDict
    net3= nn.Sequential(OrderedDict([
              ('conv1', nn.Conv2d(3, 3, 3)),
              ('bn1', nn.BatchNorm2d(3)),
              ('relu1', nn.ReLU())
            ]))
    print('net1:', net1)
    print('net2:', net2)
    print('net3:', net3)
 
 Out:net1: Sequential(
        (conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
        (batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (activation_layer): ReLU()
    )
    net2: Sequential(
        (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
        (1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (2): ReLU()
    )
    net3: Sequential(
        (conv1): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
        (bn1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (relu1): ReLU()
    )

In: # 可根据名字或序号取出子module
    net1.conv, net2[0], net3.conv1
Out:(Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
    Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
    Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)))
    
In: # 调用已构建的网络
    input = t.rand(1, 3, 4, 4)
    output = net1(input)
    output = net2(input)
    output = net3(input)
    output = net3.relu1(net1.batchnorm(net1.conv(input)))

In: modellist = nn.ModuleList([nn.Linear(3,4), nn.ReLU(), nn.Linear(4,2)])
    input = t.randn(1, 3)
    for model in modellist:
        input = model(input)
    # 下面会报错,因为modellist没有实现forward方法
    # output = modelist(input)

看到这里读者可能会问,为什么不直接使用Python中自带的list,非要多此一举呢?这是因为ModuleList是nn.Module的子类,当在module中使用它的时候,ModuleList能够自动被module识别为子module,下面举例说明:

In: class MyModule(nn.Module):
        def __init__(self):
            super().__init__()
            self.list = [nn.Linear(3, 4), nn.ReLU()]
            self.module_list = nn.ModuleList([nn.Conv2d(3, 3, 3), nn.ReLU()])
        def forward(self):
            pass
    model = MyModule()
    modelOut:MyModule(
        (module_list): ModuleList(
            (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
            (1): ReLU()
        )
    )In: for name, param in model.named_parameters():
        print(name, param.size())Out:module_list.0.weight torch.Size([3, 3, 3, 3])
    module_list.0.bias torch.Size([3])

可以看出,list中的子module不能被主module识别,ModuleList中的子module能够被主module所识别。这意味着,如果使用list保存子module,那么在反向传播时无法调整子module的参数,因为子module中的参数并没有加入到主module的参数中。

除了ModuleList还有ParameterList,它是一个可以包含多个Parameter的类似于list的对象。在实际应用中,ParameterList的使用方式与ModuleList类似。如果在构造函数__init__()中用到list、tuple、dict等对象时,那么一定要思考是否应该用ModuleList或ParameterList代替。

4. 循环神经网络

近年来,随着深度学习和自然语言处理的逐渐火热,循环神经网络(RNN)得到了广泛的关注。PyTorch中实现了最常用的三种循环神经网络:RNN(vanilla RNN)、LSTM和GRU,此外还有对应的三种RNNCell。

RNN和RNNCell层的区别在于,前者一次能够处理整个序列,后者一次只处理序列中一个时间点的数据。RNN的封装更完备,也更易于使用;RNNCell层更具灵活性,RNN层可以通过组合调用RNNCell来实现。

In: t.manual_seed(2021)
    # 输入:batch_size=3,序列长度都为2,序列中每个元素占4维
    input = t.randn(2, 3, 4).float()
    # lstm输入向量4维,3个隐藏元,1层
    lstm = nn.LSTM(4, 3, 1)
    # 初始状态:1层,batch_size=3,表示3个隐藏元
    h0 = t.randn(1, 3, 3) # 隐藏层状态(hidden state)
    c0 = t.randn(1, 3, 3) # 单元状态(cell state)
    out1, hn = lstm(input, (h0, c0))
    out1.shape
 Out:torch.Size([2, 3, 3])
 
 In: t.manual_seed(2021)
    input = t.randn(2, 3, 4).float()
    # 一个LSTMCell对应的层数只能是一层
    lstm = nn.LSTMCell(4, 3)
    hx = t.randn(3, 3)
    cx = t.randn(3, 3)
    out = []
    for i_ in input:
        hx, cx = lstm(i_, (hx, cx))
        out.append(hx)
    out2 = t.stack(out)
    out2.shape
 Out:torch.Size([2, 3, 3])

上述两种LSTM实现的结果是完全一致的。读者可以对比一下这两种实现方式有何区别,并从中体会RNN和RNNCell层的区别。

In: # 受限于精度问题,这里使用allclose函数说明结果的一致性
    out1.allclose(out2)
Out:True

词向量在自然语言中应用十分广泛,PyTorch提供了用于生成词向量的Embedding层:

In: # 有4个词,每个词用5维的向量表示
    embedding = nn.Embedding(4, 5)
    # 可以用预训练好的词向量初始化embedding
    weight = t.arange(0, 20).view(4,  5).float()
    nn.Embedding.from_pretrained(weight)
Out:Embedding(4, 5)

In: input = t.arange(3, 0, -1).long()
    output = embedding(input)
    output
 Out:tensor([[-0.6590, -2.2046, -0.1831, -0.5673,  0.6770],
            [ 1.8060,  1.0928,  0.6670,  0.4997,  0.1662],
            [ 0.1592, -0.3728, -1.1482, -0.4520,  0.5914]],
            grad_fn=<EmbeddingBackward>)

5. 损失函数

在深度学习中会经常使用各种各样的损失函数(loss function),这些损失函数可以看作是一种特殊的layer,PyTorch将这些损失函数实现为nn.Module的子类。在实际使用中通常将这些损失函数专门提取出来,作为独立的一部分。读者可以参考官方文档了解损失函数的具体用法,下面以分类问题中最常用的交叉熵损失CrossEntropyLoss为例进行讲解:

In: # batch_size=3,计算对应每个类别的分数(只有两个类别)
    score = t.randn(3, 2)
    # 三个样本分别属于1,0,1类,label必须是LongTensor
    label = t.Tensor([1, 0, 1]).long()
    
    # loss与普通的layer无差异
    criterion = nn.CrossEntropyLoss()
    loss = criterion(score, label)
    loss
Out:tensor(0.5865)

本小节对nn中的常用模块进行了详细的介绍,读者可以利用这些模块快速地搭建神经网络。在使用这些模块时应当注意每一个module所包含的参数和输入、输出的形状及含义,从而避免一些不必要的错误。

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shangjg3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值