动手学深度学习(六、卷积神经网络)

动手学深度学习(六、卷积神经网络)

原文链接:动手学深度学习

各种网络机构记录以及实验效果显示

网络结构 提出时间 实验效果 网络结构说明
LeNet 1998 test acc < 80% 提出了卷积层 + 池化层 + 全连接输出的结构
AlexNet 2012 test acc 0.882 网络结构思想同LeNet, 神经网络层数较深
VGG 2014 test acc 0.907 通过重复使用简单快来构建深层神经网络, 增加网络宽度
NiN 2013 test acc 0.782 通过串联多个由卷积层和全连接层构成的小网络来构建一个深层网络
GoogLeNet 2015年起 test acc 0.896 通过网络并行结构,使用窗口不同形状卷积层和池化层来抽取信息,模型结构极为复杂
ResNet(残差网络) 2016 test acc 0.926 在GoogLeNet网络结构上的改进,对于增加网络的深度出现的“退化"现象,将向前传输分解为两条路经结构。解决了深度网络训练困难问题
DenseNet(稠密连接网络) 2017 test acc 0.923 在ResNet提出之后, 通过定义稠密块和过渡层,使得前者和后者输出连接
批量归一化 2015 test acc 0.877 将批量归一化用于LeNet网络上,在卷积层、全连接层后添加批量归一化操作, 正确率有显著提高

(1)对于卷积神经网络中输出通道数逐渐变多的解释

我们的Out_channels要能反映图片的多种特征,所以out_channel 大于 in_channel了。

(2)在深度学习中,网络层数增多会伴随如下问题

1.计算资源的消耗    ----    通过GPU集群来解决

2.模型容易过拟合    ----    通过采集海量数据, 配合dropout正则化方法 有效避免

3.梯度消失/梯度爆炸问题的产生    ----    通过Batch Normalization(批量归一化)避免

question:在随着网络层数增加的同时, 网络发生了退化(degradation)的现象, loss逐渐下降,趋于饱和, 增加网络深度, 训练集loss反而增大

(3)在使用GPU进行模型训练时,要熟练掌握GPU环境的查看以及GPU编程操作

一、卷积神经网络(LeNet)

LeNet分为卷积层块和全连接层块两个部分。

卷积层块里的基本单位是卷积层后接最大池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的最大池化层则用来降低卷积层对位置的敏感性。卷积层块由两个这样的基本单位重复堆叠构成。卷积层比之前的卷积层的输入的高和宽要小, 所以增加输出通道使两个卷积层的参数尺寸类似。池化窗口在输入上每次滑动所覆盖的区域互不重叠。卷积层块的输出形状为(批量大小, 通道, 高, 宽)。当卷积层块的输出传入全连接层块时,全连接层块会将小批量中每个样本变平(flatten)。

在卷积层块中输入的高和宽在逐层减小。卷积层由于使用高和宽均为5的卷积核,从而将高和宽分别减小4,而池化层则将高和宽减半,但通道数则从1增加到16。全连接层则逐层减少输出个数,直到变成图像的类别数10。

所谓的权值共享就是说,给一张输入图片,用一个filter去扫这张图,filter里面的数就叫权重,这张图每个位置就是被同样的filter扫的,所以权重是一样的,也就是共享。

sigmoid 和 tanh作为激活函数的话,一定要注意一定要对 input 进行归一话,否则激活后的值都会进入平坦区,使隐层的输出全部趋同,但是 ReLU 并不需要输入归一化来防止它们达到饱和。

注:对于Conv2函数中的卷积核的值是默认生成的, 可以通过使用conv.bias、conv.weight函数来查看卷积核的值

       每个通道都是原始图像的一个抽象,堆的越多,神经网络中每一层的信息越多,原始图像的信息损失越少。

import time
import torch
from torch import nn, optim

import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        
        self.conv = nn.Sequential(
            nn.Conv2d(1, 6, 5),    #in_channels, out_channels, kernel_size
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2),    #kernel_size, stride
            
            nn.Conv2d(6, 16, 5),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2)
        )
        
        self.fc = nn.Sequential(
            nn.Linear(16 * 4 * 4, 120),
            nn.Sigmoid(),
            nn.Linear(120, 84),
            nn.Sigmoid(),
            nn.Linear(84, 10)
        )
    
    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output
    
net = LeNet()
print(net)
#在卷积层块中输入的高和宽在逐层减小。卷积层由于使用高和宽均为5的卷积核,从而将高和宽分别减小4,而池化层则将高和宽减半,但通道数则从1增加到16。全连接层则逐层减少输出个数,直到变成图像的类别数10。

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size = batch_size)

print(len(train_iter))

# 本函数已保存在d2lzh_pytorch包中方便以后使用。该函数将被逐步改进。
def evaluate_accuracy(data_iter, net, device=None):
    if device is None and isinstance(net, torch.nn.Module):
        # 如果没指定device就使用net的device
        device = list(net.parameters())[0].device
    acc_sum, n = 0.0, 0
    with torch.no_grad():
        for X, y in data_iter:
            if isinstance(net, torch.nn.Module):
                net.eval() # 评估模式, 这会关闭dropout
                acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
                net.train() # 改回训练模式
            else: # 自定义的模型, 3.13节之后不会用到, 不考虑GPU
                if('is_training' in net.__code__.co_varnames): # 如果有is_training这个参数
                    # 将is_training设置成False
                    acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item() 
                else:
                    acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() 
            n += y.shape[0]
    return acc_sum / n

# 本函数已保存在d2lzh_pytorch包中方便以后使用
def train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs):
    #device = 'cpu'
    net = net.to(device)
    print("training on ", device)
    loss = torch.nn.CrossEntropyLoss()
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n, batch_count, start = 0.0, 0.0, 0, 0, time.time()
        for X, y in train_iter:
            X = X.to(device)
            y = y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            optimizer.zero_grad()
            l.backward()
            optimizer.step()
            train_l_sum += l.cpu().item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
            n += y.shape[0]
            batch_count += 1
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec'
              % (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))

#for name, parameters in net.named_parameters():
#    print(name, ':', parameters.size())

#for parameters in net.parameters():
#    print(parameters.size())    
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr = lr)
train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)
LeNet(
  (conv): Sequential(
    (0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (1): Sigmoid()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
    (4): Sigmoid()
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): Linear(in_features=256, out_features=120, bias=True)
    (1): Sigmoid()
    (2): Linear(in_features=120, out_features=84, bias=True)
    (3): Sigmoid()
    (4): Linear(in_features=84, out_features=10, bias=True)
  )
)
235
training on  cpu
epoch 1, loss 1.8169, train acc 0.333, test acc 0.587, time 11.4 sec
epoch 2, loss 0.9421, train acc 0.635, test acc 0.689, time 11.3 sec
epoch 3, loss 0.7657, train acc 0.719, test acc 0.720, time 11.7 sec
epoch 4, loss 0.6827, train acc 0.743, test acc 0.750, time 11.9 sec
epoch 5, loss 0.6276, train acc 0.761, test acc 0.761, time 11.7 sec

二、深度卷积神经网络(AlexNet)

  • AlexNet跟LeNet结构类似,但使用了更多的卷积层和更大的参数空间来拟合大规模数据集ImageNet。它是浅层神经网络和深度神经网络的分界线。
  • 虽然看上去AlexNet的实现比LeNet的实现也就多了几行代码而已,但这个观念上的转变和真正优秀实验结果的产生令学术界付出了很多年。

当年研究者还没有大量深入研究参数初始化和非凸优化算法等诸多领域,导致复杂的神经网络的训练通常较困难。

AlexNet使用了8层卷积神经网络,并以很大的优势赢得了ImageNet 2012图像识别挑战赛。它首次证明了学习到的特征可以超越手工设计的特征,从而一举打破计算机视觉研究的前状。

AlexNet与LeNet的设计理念非常相似,但也有显著的区别。

第一,与相对较小的LeNet相比,AlexNet包含8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。下面我们来详细描述这些层的设计。

AlexNet第一层中的卷积窗口形状是11×11。因为ImageNet中绝大多数图像的高和宽均比MNIST图像的高和宽大10倍以上,ImageNet图像的物体占用更多的像素,所以需要更大的卷积窗口来捕获物体。第二层中的卷积窗口形状减小到5×5,之后全采用3×3。此外,第一、第二和第五个卷积层之后都使用了窗口形状为3×3、步幅为2的最大池化层。而且,AlexNet使用的卷积通道数也大于LeNet中的卷积通道数数十倍。

紧接着最后一个卷积层的是两个输出个数为4096的全连接层。这两个巨大的全连接层带来将近1 GB的模型参数。由于早期显存的限制,最早的AlexNet使用双数据流的设计使一个GPU只需要处理一半模型。幸运的是,显存在过去几年得到了长足的发展,因此通常我们不再需要这样的特别设计了。

第二,AlexNet将sigmoid激活函数改成了更加简单的ReLU激活函数。一方面,ReLU激活函数的计算更简单,例如它并没有sigmoid激活函数中的求幂运算。另一方面,ReLU激活函数在不同的参数初始化方法下使模型更容易训练。这是由于当sigmoid激活函数输出极接近0或1时,这些区域的梯度几乎为0,从而造成反向传播无法继续更新部分模型参数;而ReLU激活函数在正区间的梯度恒为1。因此,若模型参数初始化不当,sigmoid函数可能在正区间得到几乎为0的梯度,从而令模型无法得到有效训练。

第三,AlexNet通过丢弃法来控制全连接层的模型复杂度。而LeNet并没有使用丢弃法。

第四,AlexNet引入了大量的图像增广,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。我们将在后面的(图像增广)详细介绍这种方法。

import time
import torch
from torch import nn, optim
import torchvision

import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
            nn.ReLU(),
            nn.MaxPool2d(3, 2), # kernel_size, stride
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
         # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256*5*5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            # 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 10),
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

net = AlexNet()
print(net)

batch_size = 1
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=1)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

输出:
AlexNet(
  (conv): Sequential(
    (0): Conv2d(1, 96, kernel_size=(11, 11), stride=(4, 4))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(96, 256, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (4): ReLU()
    (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Conv2d(256, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): ReLU()
    (8): Conv2d(384, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (9): ReLU()
    (10): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU()
    (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): Linear(in_features=6400, out_features=4096, bias=True)
    (1): ReLU()
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU()
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=10, bias=True)
  )
)
training on  cuda
epoch 1, loss 0.5622, train acc 0.792, test acc 0.853, time 627.9 sec
epoch 2, loss 0.3904, train acc 0.856, test acc 0.844, time 631.2 sec
epoch 3, loss 0.3644, train acc 0.868, test acc 0.871, time 629.6 sec
epoch 4, loss 0.3512, train acc 0.872, test acc 0.869, time 628.7 sec
epoch 5, loss 0.3397, train acc 0.875, test acc 0.882, time 630.5 sec

三、使用重复元素的网络(VGG)

  • VGG-11通过5个可以重复使用的卷积块来构造网络。根据每块里卷积层个数和输出通道数的不同可以定义出不同的VGG模型。

AlexNet在LeNet的基础上增加了3个卷积层。但AlexNet作者对它们的卷积窗口、输出通道数和构造顺序均做了大量的调整。虽然AlexNet指明了深度卷积神经网络可以取得出色的结果,但并没有提供简单的规则以指导后来的研究者如何设计新的网络。VGG提出了可以通过重复使用简单的基础块来构建深度模型的思路。

VGG块的组成规律是:连续使用数个相同的填充为1、窗口形状为3×3的卷积层后接上一个步幅为2、窗口形状为2×2的最大池化层。卷积层保持输入的高和宽不变,而池化层则对其减半。我们使用vgg_block函数来实现这个基础的VGG块,它可以指定卷积层的数量和输入输出通道数。

对于给定的感受野(与输出有关的输入图片的局部大小),采用堆积的小卷积核优于采用大的卷积核,因为可以增加网络深度来保证学习更复杂的模式,而且代价还比较小(参数更少)。例如,在VGG中,使用了3个3x3卷积核来代替7x7卷积核,使用了2个3x3卷积核来代替5*5卷积核,这样做的主要目的是在保证具有相同感知野的条件下,提升了网络的深度,在一定程度上提升了神经网络的效果。

与AlexNet和LeNet一样,VGG网络由卷积层模块后接全连接层模块构成。卷积层模块串联数个vgg_block,其超参数由变量conv_arch定义。该变量指定了每个VGG块里卷积层个数和输入输出通道数。全连接模块则跟AlexNet中的一样。因为这个网络使用了8个卷积层和3个全连接层,所以经常被称为VGG-11。因为每个卷积层的窗口大小一样,所以每层的模型参数尺寸和计算复杂度与输入高、输入宽、输入通道数和输出通道数的乘积成正比。VGG这种高和宽减半以及通道翻倍的设计使得多数卷积层都有相同的模型参数尺寸和计算复杂度。

import time
import torch
from torch import nn, optim

import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

def vgg_block(num_convs, in_channels, out_channels):
    blk = []
    for i in range(num_convs):
        if i == 0:
            blk.append(nn.Conv2d(in_channels, out_channels, kernel_size = 3, padding = 1))
        else:
            blk.append(nn.Conv2d(out_channels, out_channels, kernel_size = 3, padding = 1))
        blk.append(nn.ReLU())
    
    blk.append(nn.MaxPool2d(kernel_size = 2, stride = 2))    #操作使得宽高减半
    #列表前面加星号作用是将列表中所有元素解开成独立的参数,传入函数,参数数量等于len(data)
    return nn.Sequential(*blk)

#5个卷积块,前2块使用单卷积层、后3块使用双卷积层
conv_arch = ((1, 1, 64), (1, 64, 128), (2, 128, 256), (2, 256, 512), (2, 512, 512))
#经过5个vgg_block部分, 宽高会减半5次, 从224/32 = 7
fc_features = 512 * 7 * 7    # c * w * h
fc_hidden_units = 4096

def vgg(conv_arch, fc_features, fc_hidden_units = 4096):
    net = nn.Sequential()
    #卷积层部分
    for i, (num_convs, in_channels, out_channels) in enumerate(conv_arch):
        net.add_module("vgg_block" + str(i + 1), vgg_block(num_convs, in_channels, out_channels))
    
    #全连接层部分
    net.add_module('fc', nn.Sequential(d2l.FlattenLayer(),
                                       nn.Linear(fc_features, fc_hidden_units),
                                       nn.ReLU(),
                                       nn.Dropout(0.5),
                                       nn.Linear(fc_hidden_units, fc_hidden_units),
                                       nn.ReLU(),
                                       nn.Dropout(0.5),
                                       nn.Linear(fc_hidden_units, 10)
                                      ))
    return net

net = vgg(conv_arch, fc_features, fc_hidden_units)
X = torch.rand(1, 1, 224, 224)

# named_children获取一级子模块及其名字(named_modules会返回所有子模块,包括子模块的子模块)
# for name, blk in net.named_children():
#     print(name)
#     print(blk)
    
for name, blk in net.named_children():
    X = blk(X)
    print(name, 'output shape:', X.shape)

print("*" * 100)
    

#VGG-11 计算更加复杂, 我们构造一个通道数更小, 更窄的网络在 Fashion-MNIST 上数据集进行训练
ratio = 8
small_conv_arch = [(1, 1, 64//ratio), (1, 64//ratio, 128//ratio), (2, 128//ratio, 256//ratio), (2, 256//ratio, 512//ratio), (2, 512//ratio, 512//ratio)]

net = vgg(small_conv_arch, fc_features // ratio, fc_hidden_units // ratio)

print(net)

batch_size = 64
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize = 224)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr = lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

#输出:
vgg_block1 output shape: torch.Size([1, 64, 112, 112])
vgg_block2 output shape: torch.Size([1, 128, 56, 56])
vgg_block3 output shape: torch.Size([1, 256, 28, 28])
vgg_block4 output shape: torch.Size([1, 512, 14, 14])
vgg_block5 output shape: torch.Size([1, 512, 7, 7])
fc output shape: torch.Size([1, 10])
****************************************************************************************************
Sequential(
  (vgg_block1): Sequential(
    (0): Conv2d(1, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (vgg_block2): Sequential(
    (0): Conv2d(8, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (vgg_block3): Sequential(
    (0): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU()
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (vgg_block4): Sequential(
    (0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU()
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (vgg_block5): Sequential(
    (0): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU()
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): FlattenLayer()
    (1): Linear(in_features=3136, out_features=512, bias=True)
    (2): ReLU()
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=512, out_features=512, bias=True)
    (5): ReLU()
    (6): Dropout(p=0.5, inplace=False)
    (7): Linear(in_features=512, out_features=10, bias=True)
  )
)
training on  cuda
epoch 1, loss 1.0196, train acc 0.609, test acc 0.858, time 213.3 sec
epoch 2, loss 0.3603, train acc 0.871, test acc 0.881, time 212.6 sec
epoch 3, loss 0.3079, train acc 0.888, test acc 0.890, time 213.3 sec
epoch 4, loss 0.2799, train acc 0.897, test acc 0.906, time 214.1 sec
epoch 5, loss 0.2600, train acc 0.906, test acc 0.907, time 212.8 sec

四、网络中的网络(NiN)

  • NiN重复使用由卷积层和代替全连接层的1 * 1卷积层构成的NiN块来构建深层网络。
  • NiN去除了容易造成过拟合的全连接输出层,而是将其替换成输出通道数等于标签类别数的NiN块和全局平均池化层。
  • NiN的以上设计思想影响了后面一系列卷积神经网络的设计。

LeNet、AlexNet和VGG在设计上的共同之处是:先以由卷积层构成的模块充分抽取空间特征,再以由全连接层构成的模块来输出分类结果。其中,AlexNet和VGG对LeNet的改进主要在于如何对这两个模块加宽(增加通道数)和加深。NiN提出了另外一个思路,即串联多个由卷积层和“全连接”层构成的小网络来构建一个深层网络。

NiN块

卷积层的输入和输出通常是四维数组(样本,通道,高,宽),而全连接层的输入和输出则通常是二维数组(样本,特征)。如果想在全连接层后再接上卷积层,则需要将全连接层的输出变换为四维。对于1×1卷积层,它可以看成全连接层,其中空间维度(高和宽)上的每个元素相当于样本,通道相当于特征。因此,NiN使用1*1卷积层来替代全连接层,从而使空间信息能够自然传递到后面的层中去。假设我们将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么1 * 1卷积层的作用与全连接层等价。下图对比了NiN同AlexNet和VGG等网络在结构上的主要区别。

NiN块是NiN中的基础块。它由一个卷积层加两个充当全连接层的1*1卷积层串联而成。其中第一个卷积层的超参数可以自行设置,而第二和第三个卷积层的超参数一般是固定的。

NiN模型

NiN是在AlexNet问世不久后提出的。它们的卷积层设定有类似之处。NiN使用卷积窗口形状分别为11 * 11、5 * 5和3 * 3的卷积层,相应的输出通道数也与AlexNet中的一致。每个NiN块后接一个步幅为2、窗口形状为3 * 3的最大池化层。除使用NiN块以外,NiN还有一个设计与AlexNet显著不同:NiN去掉了AlexNet最后的3个全连接层,取而代之地,NiN使用了输出通道数等于标签类别数的NiN块,然后使用全局平均池化层对每个通道中所有元素求平均并直接用于分类。这里的全局平均池化层即窗口形状等于输入空间维形状的平均池化层。NiN的这个设计的好处是可以显著减小模型参数尺寸,从而缓解过拟合。然而,该设计有时会造成获得有效模型的训练时间的增加。

全局平局池化层 对整个网路在结构上做正则化防止过拟合。其直接剔除了全连接层中黑箱的特征,直接赋予了每个channel实际的意义。但是使用gap可能会造成收敛速度减慢。

preview

import time
import torch
from torch import nn, optim
​
import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l
​
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
​
#nin_block块 输入通道 -> 输出通道
def nin_block(in_channels, out_channels, kernel_size, stride, padding):
    blk = nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding),
                        nn.ReLU(),
                        nn.Conv2d(out_channels, out_channels, kernel_size = 1),
                        nn.ReLU(),
                        nn.Conv2d(out_channels, out_channels, kernel_size = 1),
                        nn.ReLU()
                       )
    return blk
​
import torch.nn.functional as F
​
class GlobalAvgPool2d(nn.Module):
     #全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
    def __init__(self):
        super(GlobalAvgPool2d, self).__init__()
​
    def forward(self, x):
        return F.avg_pool2d(x, kernel_size = x.size()[2:])
    
net = nn.Sequential(
    nin_block(1, 96, kernel_size = 11, stride = 4, padding = 0),
    nn.MaxPool2d(kernel_size = 3, stride = 2),
    
    nin_block(96, 256, kernel_size = 5, stride = 1, padding = 2),
    nn.MaxPool2d(kernel_size = 3, stride = 2),
    
    nin_block(256, 384, kernel_size = 3, stride = 1, padding = 1),
    nn.MaxPool2d(kernel_size = 3, stride = 2),
    
    nn.Dropout(0.5),
    #标签类别为10
    nin_block(384, 10, kernel_size = 3, stride = 1, padding = 1),
    
    GlobalAvgPool2d(),
    #将四维的输出转换成二维的输出,其形状为(批量大小, 10)
    d2l.FlattenLayer()
)
​
x = torch.rand(1, 1, 224, 224)
​
for name, blk in net.named_children():
    x = blk(x)
    print(name, 'output shape:', x.shape)

batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

lr, num_epochs = 0.002, 5
optimizer = torch.optim.Adam(net.parameters(), lr = lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

​#输出:
#四维数据分别是:batch_size, 通道数, 高, 宽
0 output shape: torch.Size([1, 96, 54, 54])    #卷积层((224 - 11)/4 + 1) = 54
1 output shape: torch.Size([1, 96, 26, 26])    #池化层

2 output shape: torch.Size([1, 256, 26, 26])    #卷积层
3 output shape: torch.Size([1, 256, 12, 12])    #池化层

4 output shape: torch.Size([1, 384, 12, 12])    #卷积层
5 output shape: torch.Size([1, 384, 5, 5])    #池化层

6 output shape: torch.Size([1, 384, 5, 5])    #dropout层
7 output shape: torch.Size([1, 10, 5, 5])    #卷积层
8 output shape: torch.Size([1, 10, 1, 1])    #平均池化层

9 output shape: torch.Size([1, 10])    #输出向量维度改变

training on  cuda
epoch 1, loss 1.1685, train acc 0.594, test acc 0.720, time 286.0 sec
epoch 2, loss 0.7940, train acc 0.734, test acc 0.730, time 291.7 sec
epoch 3, loss 0.7313, train acc 0.749, test acc 0.757, time 293.0 sec
epoch 4, loss 0.6737, train acc 0.772, test acc 0.774, time 292.4 sec
epoch 5, loss 0.6482, train acc 0.783, test acc 0.782, time 292.4 sec

pytorch中的池化层(最大池化 torch.nn.MaxPool2d,    平均池化 torch.nn.functional.avg_pool2d)

五、含并行连结的网络(GoogLeNet)

  • Inception块相当于一个有4条线路的子网络。它通过不同窗口形状的卷积层和最大池化层来并行抽取信息,并使用1×1卷积层减少通道数从而降低模型复杂度。
  • GoogLeNet将多个设计精细的Inception块和其他层串联起来。其中Inception块的通道数分配之比是在ImageNet数据集上通过大量的实验得来的。
  • GoogLeNet和它的后继者们一度是ImageNet上最高效的模型之一:在类似的测试精度下,它们的计算复杂度往往更低。

增强网络能力:增加网络深度、增加网络宽度

减少过拟合:减少自由参数

模型原理:【1】使用不同size的卷积核的感受野做特征提取,是一种混合结构, Max Pooling也做特征提取(没有参数不会过拟合), 1 * 1 卷积降低input的channel数量(可以使得网络变深, 同时计算量反而变小(1 * 1卷积核起到降低特征图厚度作用))【2】使用两层堆叠的3 * 3代替一层 5 * 5, 使得参数量少了, 计算量少了, 层数变深了, 效果变好了 【3】Inception模块结合 ResNet结构可以极大地加速训练, 同时性能也有所提升。

Inception块

GoogLeNet中的基础卷积块叫作Inception块, 如下图所示, Inception块里有4条并行的线路, 前3条线路使用窗口大小分别是1 * 1, 3 * 3, 5 * 5的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做1 * 1卷积来减少输入通道数,以降低模型复杂度。第四条线路则使用3 * 3最大池化层,后接1 * 1卷积层来改变通道数。4条线路都使用了合适的填充来使输入与输出的高和宽一致。最后将每条线路的输出在通道维上连结,并输入接下来的层中去。Inception块中可以自定义的超参数是每个层的输出通道数, 以此来控制模型复杂度。

GoogLeNet模型

GoogLeNet跟VGG一样,在主体卷积部分中使用5个模块(block),每个模块之间使用步幅为2的3 * 3最大池化层来减小输出高宽。

第一模块使用一个64通道的7×7卷积层。

第二模块使用2个卷积层:首先是64通道的1×1卷积层,然后是将通道增大3倍的3×3卷积层。它对应Inception块中的第二条线路。

第三模块串联2个完整的Inception块。第一个Inception块的输出通道数为64+128+32+32=256,其中4条线路的输出通道数比例为64:128:32:32=2:4:1:1。其中第二、第三条线路先分别将输入通道数减小至96/192=1/2和16/192=1/12后,再接上第二层卷积层。第二个Inception块输出通道数增至128+192+96+64=480,每条线路的输出通道数之比为128:192:96:64=4:6:3:2。其中第二、第三条线路先分别将输入通道数减小至128/256=1/2和32/256=1/8。

第四模块更加复杂。它串联了5个Inception块,其输出通道数分别是192+208+48+64=512、160+224+64+64=512、128+256+64+64=512、112+288+64+64=528和256+320+128+128=832。这些线路的通道数分配和第三模块中的类似,首先含3×3卷积层的第二条线路输出最多通道,其次是仅含1×1卷积层的第一条线路,之后是含5×5卷积层的第三条线路和含3×3最大池化层的第四条线路。其中第二、第三条线路都会先按比例减小通道数。这些比例在各个Inception块中都略有不同。

第五模块有输出通道数为256+320+128+128=832和384+384+128+128=1024的两个Inception块。其中每条线路的通道数的分配思路和第三、第四模块中的一致,只是在具体数值上有所不同。需要注意的是,第五模块的后面紧跟输出层,该模块同NiN一样使用全局平均池化层来将每个通道的高和宽变成1。最后我们将输出变成二维数组后接上一个输出个数为标签类别数的全连接层。

GoogLeNet模型的计算复杂,而且不如VGG那样便于修改通道数。

import time
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class Inception(nn.Module):
    # c1 - c4 为每条线路里的层的输出通道数
    def __init__(self, in_c, c1, c2, c3, c4):
        super(Inception, self).__init__()
        
        #线路1, 单1 * 1卷积层
        self.p1_1 = nn.Conv2d(in_c, c1, kernel_size = 1)
        
        #线路2, 1 * 1卷积层 后接 3 * 3卷积层
        self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size = 1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size = 3, padding = 1)
        
        #线路3, 1 * 1卷积层 后接 5 * 5卷积层
        self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size = 1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size = 5, padding = 2)
        
        #线路4, 3 * 3最大池化层 后接 1 * 1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size = 3, stride = 1, padding = 1)
        self.p4_2 = nn.Conv2d(in_c, c4, kernel_size = 1)
        
    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        return torch.cat((p1, p2, p3, p4), dim = 1) #在通道维上连结输出
    
#GoogLeNet跟VGG一样,在主体卷积部分中使用5个模块(block),每个模块之间使用步幅为2的3×33×3最大池化层来减小输出高宽。
#第一模块使用一个64通道的7×7卷积层。
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size = 7, stride = 2, padding = 3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)
                  )

#第二模块使用2个卷积层:首先是64通道的1×1卷积层,然后是将通道增大3倍的3×3卷积层。它对应Inception块中的第二条线路。
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size = 1),
                   nn.Conv2d(64, 192, kernel_size = 3, padding = 1),
                   nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)
                  )

#第三模块串联两个完整的Inception块
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                   Inception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

#第四模块更加复杂。它串联了5个Inception块
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
                   Inception(512, 160, (112, 224), (24, 64), 64),
                   Inception(512, 128, (128, 256), (24, 64), 64),
                   Inception(512, 112, (144, 288), (32, 64), 64),
                   Inception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

#d第五模块串联了2个Inception块, 同时在后面紧跟输出层
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                   Inception(832, 384, (192, 384), (48, 128), 128),
                   d2l.GlobalAvgPool2d())

net = nn.Sequential(b1, b2, b3, b4, b5, 
                    d2l.FlattenLayer(), 
                    nn.Linear(1024, 10))

X = torch.rand(1, 1, 96, 96)
for blk in net.children(): 
    X = blk(X)
    print('output shape: ', X.shape)

batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

#输出:
output shape:  torch.Size([1, 64, 24, 24])
output shape:  torch.Size([1, 192, 12, 12])
output shape:  torch.Size([1, 480, 6, 6])
output shape:  torch.Size([1, 832, 3, 3])
output shape:  torch.Size([1, 1024, 1, 1])
output shape:  torch.Size([1, 1024])
output shape:  torch.Size([1, 10])
training on  cuda
epoch 1, loss 0.9715, train acc 0.621, test acc 0.822, time 184.5 sec
epoch 2, loss 0.4137, train acc 0.846, test acc 0.849, time 184.0 sec
epoch 3, loss 0.3379, train acc 0.874, test acc 0.874, time 184.3 sec
epoch 4, loss 0.2927, train acc 0.892, test acc 0.873, time 187.4 sec
epoch 5, loss 0.2674, train acc 0.901, test acc 0.896, time 186.6 sec

六、残差网络(ResNet)

  • 残差块通过跨层的数据通道从而能够训练出有效的深度神经网络。
  • ResNet深刻影响了后来的深度神经网络的设计。

对神经网络模型添加新的层,充分训练后的模型是否只可能更有效地降低训练误差?理论上,原模型解的空间只是新模型解的空间的子空间。也就是说,如果我们能将新添加的层训练成恒等映射f(x)=x,新模型和原模型将同样有效。

由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。然而在实践中,添加过多的层后训练误差往往不降反升。即使利用批量归一化带来的数值稳定性使训练深层模型更加容易,该问题仍然存在。针对这一问题,何恺明等人提出了残差网络(ResNet),在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。

从信息论的角度讲,由于DPI(数据处理不等式)的存在,在前向传输的过程中,随着层数的加深,Feature Map包含的图像信息会逐层减少,而ResNet的直接映射的加入,保证了 L + 1 层的网络一定比 L 层包含更多的图像信息。基于这种使用直接映射来连接网络不同层直接的思想,残差网络应运而生。

残差块

残差网络由一系列残差块组成, 一个残差块可以表示为 x_{l+1} = x_{l} + F(x_{l}, W_{l})。 残差块分为两部分 直接映射部分 和 残差部分, h(x_{l}) 是直接映射, F(x_{l}, W_{l}) 是残差部分, 由于x_{l} 可能 和 x_{l + 1} 的特征图的数量不一样, 就需要使用 1 * 1卷积 进行升维 或者 降维, 此时残差块表示为 x_{l+1} = h(x_{l}) + F(x_{l}, W_{l}), 其中 h(x_{l}) = W_{l}^{'}x, 其中W_{l}^{'} 是1 * 1 卷积操作。实验结果表明 1 * 1卷积对模型性能提升有限,一般是在升维或者降维时才会使用。

当理想映射f(x)极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。右图是ResNet的基础块, 及残差块。在残差块中, 输入可以通过跨层的数据线路更快地向前传播。 ResNet沿用了VGG全3×3卷积层的设计。残差块里首先有2个有相同输出通道数的3×3卷积层。每个卷积层后接一个批量归一化层和ReLU激活函数。然后我们将输入跳过这两个卷积运算后直接加在最后的ReLU激活函数前。这样的设计要求两个卷积层的输出与输入形状一样,从而可以相加。如果想改变通道数,就需要引入一个额外的1×1卷积层来将输入变换成需要的形状后再做相加运算。

残差块地实现。设定输出通道数、是否使用额外的1 * 1卷积层 来修改通道数以及卷积层地步幅

如下代码实现使用两条路径进行特征提取 

路径1:卷积层(in_channels  --> out_channels)  -->  批量归一化层  -->  激活函数  -->  卷积层(out_channels  -->  out_channels)  --> 批量归一化层  

路径2:倘若输入输出通道数不相同, 使用1 * 1卷积层进行通道数对齐

结合:路径1结果 + 路径2结果

ResNet模型

ResNet的前两层与之前介绍的GoogleNet中一样:在输出通道数为64, 步幅2的为7 * 7的卷积层后接 步幅为2的3 * 3的最大池化层。不同之处在于ResNet每个卷积层后增加的批量归一化层。GoogLeNet在后面接了4个由Inception块组成的模块。ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。第一个模块的通道数同输入通道数一致。由于之前已经使用了步幅为2的最大池化层,所以无须减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。接着我们为ResNet加入所有残差块。这里每个模块使用两个残差块。最后,与GoogLeNet一样,加入全局平均池化层后接上全连接层输出。

这里每个模块里有4个卷积层(不计算1×1卷积层),加上最开始的卷积层和最后的全连接层,共计18层。这个模型通常也被称为ResNet-18。通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。虽然ResNet的主体架构跟GoogLeNet的类似,但ResNet结构更简单,修改也更方便。这些因素都导致了ResNet迅速被广泛使用。

import time
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class Residual(nn.Module):
    #输入通道、输出通道、是否使用1 * 1卷积核、步幅
    def __init__(self, in_channels, out_channels, use_1_1conv = False, stride = 1):
        super(Residual, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size = 3, padding = 1, stride = stride)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size = 3, padding = 1)
        
        if use_1_1conv:
            self.conv3 = nn.Conv2d(in_channels, out_channels, kernel_size = 1, stride = stride)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
    
    def forward(self, x):
        y = F.relu(self.bn1(self.conv1(x)))
        y = self.bn2(self.conv2(y))
        
        if self.conv3:
            x = self.conv3(x)
        return F.relu(y + x)

#查看输入输出通道数一致的情况
x = torch.rand((4, 3, 6, 6))

blk = Residual(3, 3)
print(blk(x).shape)
print("*" * 30)

#增加输出通道数的同时减半输出的高和宽
blk = Residual(3, 6, use_1_1conv = True, stride = 2)
print(blk(x).shape)
print("*" * 30)

net = nn.Sequential(
    nn.Conv2d(1, 64, kernel_size = 7, stride = 2, padding = 3),
    nn.BatchNorm2d(64),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)
)

def resnet_block(in_channels, out_channels, num_residuals, first_block = False):
    if first_block:
        assert in_channels == out_channels #第一个模块的通道数 同输入通道数 一致
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:
            blk.append(Residual(in_channels, out_channels, use_1_1conv = True, stride = 2))
        else:
            blk.append(Residual(out_channels, out_channels))
    #第二次出现列表前 加 星号
    #将列表解开成独立的参数,传入函数,还有类似的有两个星号,是将字典解开成独立的元素作为形参。
    return nn.Sequential(*blk)

net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block = True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))

net.add_module("global_avg_pool", d2l.GlobalAvgPool2d())#GlobalAvgPool2d的输出:(Batch, 512, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(512, 10))) #全连接层 将 数据按照一维向量展平, 连接10分类层

x = torch.rand(1, 1, 224, 224)
for name, layer in net.named_children():
    x = layer(x)
    print(name, ' output shape:\t', x.shape)
print("*" * 30)

batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)


#输出:

torch.Size([4, 3, 6, 6])    #输入输出通道数是相同的
******************************
torch.Size([4, 6, 3, 3])    #输出通道数翻倍, 宽高减半
******************************
0  output shape:	 torch.Size([1, 64, 112, 112])
1  output shape:	 torch.Size([1, 64, 112, 112])
2  output shape:	 torch.Size([1, 64, 112, 112])
3  output shape:	 torch.Size([1, 64, 56, 56])    #前四层为初始设置的网络结构
resnet_block1  output shape:	 torch.Size([1, 64, 56, 56])
resnet_block2  output shape:	 torch.Size([1, 128, 28, 28])
resnet_block3  output shape:	 torch.Size([1, 256, 14, 14])
resnet_block4  output shape:	 torch.Size([1, 512, 7, 7])    #添加了4个残差网络结构
global_avg_pool  output shape:	 torch.Size([1, 512, 1, 1])    #进行了全局池化
fc  output shape:	 torch.Size([1, 10])    #全连接层输出
******************************
training on  cuda
epoch 1, loss 0.3831, train acc 0.857, test acc 0.896, time 188.0 sec
epoch 2, loss 0.2482, train acc 0.909, test acc 0.902, time 190.1 sec
epoch 3, loss 0.2102, train acc 0.922, test acc 0.909, time 187.8 sec
epoch 4, loss 0.1833, train acc 0.931, test acc 0.908, time 187.9 sec
epoch 5, loss 0.1567, train acc 0.941, test acc 0.926, time 188.9 sec

七、稠密连接网络(DenseNet)

  • 在跨层连接上,不同于ResNet中将输入与输出相加,DenseNet在通道维上连结输入与输出。
  • DenseNet的主要构建模块是稠密块和过渡层。

ResNet中的跨层连接设计引申出了数个后续工作。其中的一个:稠密连接网络(DenseNet)与ResNet的主要区别如图所示。

将部分前后相邻的运算抽象为模块A和模块B。与ResNet的主要区别在于,DenseNet里模块B的输出不是像ResNet那样和模块A的输出相加,而是在通道维上连结。这样模块A的输出可以直接传入模块B后面的层。在这个设计里,模块A直接跟模块B后面的所有层连接在了一起。这也是它被称为“稠密连接”的原因。DenseNet的主要构建模块是稠密块(dense block)和过渡层(transition layer)。前者定义了输入和输出是如何连结的,后者则用来控制通道数,使之不过大。

稠密块

DenseNet使用了ResNet改良版的“批量归一化、激活和卷积”结构,  在conv_block函数里实现这个结构。

稠密块由多个conv_block组成, 每块使用相同的输出通道数。在前向计算时, 将每块的输入和输出在通道维上连结。

卷积块的通道数控制了输出通道数相对于输入通道数的增长,因此也被称为增长率(growth rate)。

过渡层

由于每个稠密块都会带来通道数的增加,使用过多则会带来过于复杂的模型。过渡层用来控制模型复杂度。它通过1×1卷积层来减小通道数,并使用步幅为2的平均池化层减半高和宽,从而进一步降低模型复杂度。

DenseNet模型

DenseNet首先使用同ResNet一样的单卷积层和最大池化层。

类似于ResNet接下来使用的4个残差块,DenseNet使用的是4个稠密块。同ResNet一样,我们可以设置每个稠密块使用多少个卷积层。这里我们设成4,从而与上一节的ResNet-18保持一致。稠密块里的卷积层通道数(即增长率)设为32,所以每个稠密块将增加128个通道。ResNet里通过步幅为2的残差块在每个模块之间减小高和宽。这里我们则使用过渡层来减半高和宽,并减半通道数。

import time
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

#conv_block函数结构里面实现了通道数由in_channels 到 out_channels 的转换
def conv_block(in_channels, out_channels):
    blk = nn.Sequential(nn.BatchNorm2d(in_channels),
                        nn.ReLU(),
                        nn.Conv2d(in_channels, out_channels, kernel_size = 3, padding = 1))
    return blk

class DenseBlock(nn.Module):
    def __init__(self, num_convs, in_channels, out_channels):
        super(DenseBlock, self).__init__()
        net = []
        for i in range(num_convs):
            in_c = in_channels + i * out_channels
            net.append(conv_block(in_c, out_channels))
        self.net = nn.ModuleList(net)
        self.out_channels = in_channels + num_convs * out_channels    #计算输出通道数
        
    def forward(self, x):
        for blk in self.net:
            y = blk(x)
            x = torch.cat((x, y), dim = 1)
        return x
    
#定义一个有2个输出通道数为10的卷积块, 输入通道数为3时, 输出通道数为3 + 2 * 10
blk = DenseBlock(2, 3, 10)
x = torch.rand(4, 3, 8, 8)
y = blk(x)
print(y.shape)
    
#过渡层
def transition_block(in_channels, out_channels):
    blk = nn.Sequential(
        nn.BatchNorm2d(in_channels),
        nn.ReLU(),
        nn.Conv2d(in_channels, out_channels, kernel_size = 1),
        nn.AvgPool2d(kernel_size = 2, stride = 2)
    )
    return blk

blk = transition_block(23, 10)
print(blk(y).shape)

#DenseNet模型首先使用同ResNet一样的单卷积层 和 最大池化层
net = nn.Sequential(
    nn.Conv2d(1, 64, kernel_size = 7, stride = 2, padding = 3),
    nn.BatchNorm2d(64),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)
)

num_channels, growth_rate = 64, 32 # num_channels为当前的通道数
#使用4个稠密块, 每个稠密块设置4个卷积层
num_convs_in_dense_blocks = [4, 4, 4, 4]

for i, num_convs in enumerate(num_convs_in_dense_blocks):
    #定义num_convs个输出通道数为growth_rate的卷积块
    DB = DenseBlock(num_convs, num_channels, growth_rate)
    net.add_module("DenseBlock_%d" % i, DB)
    #上一个稠密块的输出通道数
    num_channels = DB.out_channels
    #在稠密块之间加入通道数减半的过渡层, transition_block过渡层减少通道数和尺寸大小
    if i != len(num_convs_in_dense_blocks) - 1:
        net.add_module("transition_block_%d" % i, transition_block(num_channels, num_channels // 2))
        num_channels = num_channels // 2
#同ResNet一样, 最后接上 全局池化层 和 全连接层 来输出
net.add_module("BN", nn.BatchNorm2d(num_channels))
net.add_module("relu", nn.ReLU())
net.add_module("global_avg_pool", d2l.GlobalAvgPool2d())    #GlobalAvgPool2d的输出(Batch, num_channels, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(num_channels, 10)))

X = torch.rand((1, 1, 96, 96))
for name, layer in net.named_children():
    X = layer(X)
    print(name, 'output shape:\t', X.shape)
    
batch_size = 64
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

#输出:
torch.Size([4, 23, 8, 8])
torch.Size([4, 10, 4, 4])
0 output shape:	 torch.Size([1, 64, 48, 48])
1 output shape:	 torch.Size([1, 64, 48, 48])
2 output shape:	 torch.Size([1, 64, 48, 48])
3 output shape:	 torch.Size([1, 64, 24, 24])
DenseBlock_0 output shape:	 torch.Size([1, 192, 24, 24])
transition_block_0 output shape:	 torch.Size([1, 96, 12, 12])
DenseBlock_1 output shape:	 torch.Size([1, 224, 12, 12])
transition_block_1 output shape:	 torch.Size([1, 112, 6, 6])
DenseBlock_2 output shape:	 torch.Size([1, 240, 6, 6])
transition_block_2 output shape:	 torch.Size([1, 120, 3, 3])
DenseBlock_3 output shape:	 torch.Size([1, 248, 3, 3])
BN output shape:	 torch.Size([1, 248, 3, 3])
relu output shape:	 torch.Size([1, 248, 3, 3])
global_avg_pool output shape:	 torch.Size([1, 248, 1, 1])
fc output shape:	 torch.Size([1, 10])
training on  cuda
epoch 1, loss 0.4172, train acc 0.848, test acc 0.874, time 143.8 sec
epoch 2, loss 0.2672, train acc 0.901, test acc 0.894, time 143.8 sec
epoch 3, loss 0.2302, train acc 0.916, test acc 0.902, time 143.8 sec
epoch 4, loss 0.2069, train acc 0.924, test acc 0.916, time 144.4 sec
epoch 5, loss 0.1885, train acc 0.930, test acc 0.923, time 144.1 sec

八、批量归一化方法

  • 在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络的中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。
  • 对全连接层和卷积层做批量归一化的方法稍有不同。
  • 批量归一化层和丢弃层一样,在训练模式和预测模式的计算结果是不一样的
  • PyTorch提供了BatchNorm类方便使用。

批量归一化(batch normalization)层,它能让较深的神经网络的训练变得更加容易。我们在对输入数据做了标准化处理:处理后的任意一个特征在数据集中所有样本上的均值为0、标准差为1。标准化处理输入数据使各个特征的分布相近:这往往更容易训练出有效的模型。

数据标准化预处理对于浅层模型就足够有效了。随着模型训练的进行,当每层中参数更新时,靠近输出层的输出较难出现剧烈变化。但对深层神经网络来说,即使输入数据已做标准化,训练中模型参数的更新依然很容易造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常难以训练出有效的深度模型。解决深度神经网络靠近输出层产生剧烈变化的情况

批量归一化的提出正是为了应对深度模型训练的挑战。在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。批量归一化和残差网络为训练和设计深度模型提供了两类重要思路

1、批量归一化层

对 全连接层 和 卷积层 做批量归一化的方法不同

对全连接层做批量归一化

将批量归一化层置于全连接层中的仿射变换和激活函数之间。设全连接层的输入为u,权重参数和偏差参数分别为Wb,激活函数为\Phi。设批量归一化的运算符为BN。那么,使用批量归一化的全连接层的输出为    \Phi \left ( BN\left ( x \right ) \right ), 其中批量归一化输入x由仿射变换 x = Wu + b 得到。

对于批量归一化层输入样本 x^{(i)}, 批量归一化层的输出同样是 d 维向量 y^{(i)} = BN(x^{(i)})首先对

首先对小批量样本\beta=\left \{ x^{(1)}, x^{(2)},... ,x^{(m)} \right \}求均值和方差:

 接下来使用按元素开方和按元素除法对x^{(i)}标准化:

其中 ϵ > 0是一个很小的常数, 保证分母大于0。在标准化的基础上, 批量归一化层引入两个可以学习的模型参数, 拉伸(scale)参数\gamma 和偏移(shift)参数\beta, 这两个参数和x^{(i)}形状相同, 皆为d维向量。

值得注意的是,可学习的拉伸和偏移参数保留了不对x^{(i)}做批量归一化的可能:此时只需学出

\gamma = \sqrt{\sigma _{B}^{2} + \epsilon }\beta = \mu _{B}

​我们可以对此这样理解:如果批量归一化无益,理论上,学出的模型可以不使用批量归一化。

对卷积层做批量归一化

对卷积层来说,批量归一化发生在卷积计算之后、应用激活函数之前。如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数,并均为标量。设小批量中有m个样本。在单个通道上,假设卷积计算输出的高和宽分别为p和q。我们需要对该通道中m×p×q个元素同时做批量归一化。对这些元素做标准化计算时,我们使用相同的均值和方差,即该通道中m×p×q个元素的均值和方差。

预测时的批量归一化

使用批量归一化训练时,我们可以将批量大小设得大一点,从而使批量内样本的均值和方差的计算都较为准确。将训练好的模型用于预测时,我们希望模型对于任意输入都有确定的输出。因此,单个样本的输出不应取决于批量归一化所需要的随机小批量中的均值和方差。一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。可见,和丢弃层一样,批量归一化层在训练模式和预测模式下的计算结果也是不一样的。

2.从零开始实现

import time
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

#实现批量归一化层
#是否训练模式, 输入参数, 拉伸、偏移参数, 移动平均值, 移动方差, 常数, 移动率
def batch_norm(is_training, X, gamma, beta, moving_mean, moving_var, eps, momentum):
    
    if not is_training:
        #如果是在预测模式下, 直接使用传入的移动平均所得的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        #如果X的维度是2 或者 4
        assert len(X.shape) in (2, 4)
        if len(X.shape) == 2:
            #使用全连接层的情况, 计算特征维上的均值和方差
            mean = X.mean(dim = 0)
            var = ((X - mean) ** 2).mean(dim = 0)
        else:
            #使用二维卷积层的情况, 计算通道维上(axis = 1)的均值和方差, keepdim 为保持矩阵的维度特性
            #X的形状以便后面可以做广播运算
            mean = X.mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
            var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
            
        #训练模式下用当前的均值和方差做标准化
        X_hat = (X - mean) / torch.sqrt(var + eps)
        
        #更新 移动 平均的均值和方差
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
    Y = gamma * X_hat + beta #拉伸 和 偏移
    return Y, moving_mean, moving_var

#自定义一个BatchNorm层, 保存参与求梯度和迭代的拉伸参数gamma 和偏移参数 beta, 同时维护移动平均得到的均值和方差
#num_features 对于全连接层来说 为输出个数, 对卷积层来说 为输出通道数; num_dims参数对于全连接层和卷积层来说分别为2,4
class BatchNorm(nn.Module):
    def __init__(self, num_features, num_dims):
        super(BatchNorm, self).__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
        #参与求梯度和迭代的 拉伸和偏移 参数, 分别初始化为0 和 1
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        #不参与求梯度和迭代的变量,在内存上初始化为全0
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.zeros(shape)
    
    def forward(self, X):
        # 如果X不在内存上,将moving_mean和moving_var复制到X所在显存上
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device)
            self.moving_var = self.moving_var.to(X.device)
            
        # 保存更新过的moving_mean和moving_var, Module实例的traning属性默认为true, 调用.eval()后设成false     
        Y, self.moving_mean, self.moving_var = batch_norm(self.training, 
            X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.9)
        
        return Y
    
#重新定义LeNet网络结构
net = nn.Sequential(
            nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
            BatchNorm(6, num_dims=4),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2), # kernel_size, stride
    
            nn.Conv2d(6, 16, 5),
            BatchNorm(16, num_dims=4),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2),
    
            d2l.FlattenLayer(),
    
            nn.Linear(16*4*4, 120),
            BatchNorm(120, num_dims=2),
            nn.Sigmoid(),
    
            nn.Linear(120, 84),
            BatchNorm(84, num_dims=2),
            nn.Sigmoid(),
    
            nn.Linear(84, 10)
        )

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

#查看第一个批量归一化层学习到的拉伸参数 gamma 和 偏移参数 beta
net[1].gamma.view((-1, )), net[1].beta.view((-1, ))

#输出:
training on  cuda
epoch 1, loss 0.9936, train acc 0.788, test acc 0.827, time 8.4 sec
epoch 2, loss 0.4586, train acc 0.864, test acc 0.842, time 8.5 sec
epoch 3, loss 0.3665, train acc 0.879, test acc 0.869, time 8.2 sec
epoch 4, loss 0.3326, train acc 0.886, test acc 0.843, time 8.3 sec
epoch 5, loss 0.3105, train acc 0.891, test acc 0.877, time 8.3 sec
(tensor([0.9788, 1.1194, 1.1553, 0.9775, 0.9793, 0.9587], device='cuda:0',
        grad_fn=<ViewBackward>),
 tensor([-0.6894,  0.2823,  0.2267, -0.6877, -0.4428, -0.6768], device='cuda:0',
        grad_fn=<ViewBackward>))

3.简洁实现

Pytorch中nn模块定义的 BatchNorm1d、BatchNorm2d 类使用起来更加简单, 二者分别用于全连接层 和 卷积层, 都需要指定输入的num_features 参数值, 直接使用一行代码代替了两个模块的功能实现。

import time
import torch
from torch import nn, optim
import torch.nn.functional as F

import sys
sys.path.append("F:\数据\MRC\动手学深度学习\Dive-into-DL-PyTorch-master\code")
import d2lzh_pytorch as d2l

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')


net = nn.Sequential(
            nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
            #BatchNorm(6, num_dims=4),
            nn.BatchNorm2d(6),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2), # kernel_size, stride
            nn.Conv2d(6, 16, 5),
            #BatchNorm(16, num_dims=4),
            nn.BatchNorm2d(16),
            nn.Sigmoid(),
            nn.MaxPool2d(2, 2),
            d2l.FlattenLayer(),
            nn.Linear(16*4*4, 120),
            #BatchNorm(120, num_dims=2),
            nn.BatchNorm1d(120),
            nn.Sigmoid(),
            nn.Linear(120, 84),
            #BatchNorm(84, num_dims=2),
            nn.BatchNorm1d(84),
            nn.Sigmoid(),
            nn.Linear(84, 10)
    )

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

#输出:
training on  cuda
epoch 1, loss 1.0035, train acc 0.782, test acc 0.829, time 6.9 sec
epoch 2, loss 0.4624, train acc 0.861, test acc 0.850, time 7.1 sec
epoch 3, loss 0.3727, train acc 0.874, test acc 0.847, time 6.8 sec
epoch 4, loss 0.3382, train acc 0.882, test acc 0.867, time 6.8 sec
epoch 5, loss 0.3117, train acc 0.890, test acc 0.877, time 6.8 sec

猜你喜欢

转载自blog.csdn.net/jiangchao98/article/details/115362067