卷积神经网络 (convolutional neural networks,CNN)

神经网络架构

  1. 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。
  2. 局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。

多输入多输出通道的互相关运算(卷积)

当输入*(cinhnw)包含 ci 通道时,需要构造一个与输入数据具有相同输入通道数的卷积核 (cikhkw)* ,以便与输入数据进行互相关运算。
我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将得的结果相加)得到二维张量。(即得到了一个通道的输出)

当输出 (cohw) 包含co通道时,需要co个 (cikhkw) 的卷积核,即卷积核的形状为 (cocikhkw)*

例:输入通道为2,输出通道为1
在这里插入图片描述
1*1卷积层:通常用于调整网络层的通道数量和控制模型复杂性
在这里插入图片描述

卷积本质

卷积的本质是有效提取相邻像素间的相关特征

填充和步幅

**填充:**在输入图像的边界填充元素(通常填充元素是0)。填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
**步幅:**每次滑动元素的数量称为步幅(stride)。步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的1/n(n是一个大于的整数)。
填充和步幅可用于有效地调整数据的维度。
在这里插入图片描述
其中ph表示一共填充的行数,pw表示一共填充的列数
注意:nn.Conv2d中的padding=1时 这里的ph=2padding=2, pw=2padding=2
padding=(1,3) ph=21=2,pw=23=6
例:
在这里插入图片描述
如图是输入(高宽nh=3,nw=3),上下左右各填充一行(ph=2,pw=2,padding=1),与2*2卷积核(高宽kh=2,kw=2),垂直步幅为3(sh=3),水平步幅为2 (sw=2) 的二维互相关运算

输出形状为[ (3-2+2+3)/3 ][ (3-2+2+2)/2] =(22)
故输出形状为2*2

注:

1、在卷积神经网络中,我们组合使用卷积层、非线性激活函数和汇聚层。
2、为了构造高性能的卷积神经网络,我们通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数。

卷积神经网络

这些模型包括:
• LeNet。最早的卷积神经网络之一;
• AlexNet。它是第⼀个在大规模视觉竞赛中击败传统计算机视觉模型的大型神经网络;
• 使用重复块的网络(VGG)。它利用许多重复的神经网络块;
• 网络中的网络(NiN)。它重复使用由卷积层和1 × 1卷积层(用来代替全连接层)来构建深层网络;
• 含并行连结的网络(GoogLeNet)。它使用并行连结的网络,通过不同窗口大小的卷积层和最大汇聚层来并行抽取信息;
• 残差网络(ResNet)。它通过残差块构建跨层的数据通道,是计算机视觉中最流行的体系架构;
• 稠密连接网络(DenseNet)。它的计算成本很高,但给我们带来了更好的效果。

一、LeNet

总体来看,LeNet(LeNet-5)由两个部分组成:
• 卷积编码器:由两个卷积层组成;
• 全连接层密集块:由三个全连接层组成。
在这里插入图片描述
在这里插入图片描述

import torch
from d2l import torch as d2l
from torchvision import transforms
from torch import nn 
import torchvision
from torch.utils import data

#整合所有组件
#该函数获取和读取Fashion-MNIST数据集,这个函数返回训练集和验证集的数据迭代器
def load_data_fashion_mnist(batch_size,resize=None):#@save
    #下载Fashion-MNIST数据集,然后将其加载到内存中
    trans=[transforms.ToTensor()]
    if resize:
        #trans=[torchvision.transform.Resize(resize),torchvision.transform.ToTensor()]
        trans.insert(0,transforms.Resize(resize))
    trans=transforms.Compose(trans)
    mnist_train=torchvision.datasets.FashionMNIST(root="F:/dataset",train=True,transform=trans,download=True)
    mnist_test=torchvision.datasets.FashionMNIST(root="F:/dataset",train=False,transform=trans,download=True)
    return data.DataLoader(mnist_train,batch_size,shuffle=True,num_workers=d2l.get_dataloader_workers()),data.DataLoader(mnist_test,batch_size,shuffle=False,num_workers=d2l.get_dataloader_workers())

def evaluate_accuracy_gpu(net,data_iter,device=None):#@save
    #使用GPU计算模型在数据集上的精度
    if isinstance(net,nn.Module):
        net.eval()#设置为评估模式
        if not device:
            device=next(iter(net.parameters())).device
    #正确预测的数据,总预测的数量
    metric=d2l.Accumulator(2)
    with torch.no_grad():
        for X,y in data_iter:
            if isinstance(X,list):
                #BERT微调所需要的
                X=[x.to(device) for x in X]
            else:
                X=X.to(device)
            y=y.to(device)
            metric.add(d2l.accuracy(net(X),y),y.numel())
    return metric[0]/metric[1]

#定义LeNet的神经网络
#注意平均池化成的stride=2
net=nn.Sequential(nn.Conv2d(1,6,kernel_size=5,padding=2),nn.ReLU(),nn.AvgPool2d(stride=2,kernel_size=2),
                 nn.Conv2d(6,16,kernel_size=5,padding=0),nn.ReLU(),nn.AvgPool2d(stride=2,kernel_size=2),
                 nn.Flatten(),nn.Linear(16*5*5,120),nn.ReLU(),nn.Linear(120,84),nn.ReLU(),nn.Linear(84,10))
X=torch.rand((1,1,28,28),dtype=torch.float32)
for layer in net:
    X=layer(X)
    print(layer.__class__.__name__,"output shape:\t",X.shape)

输出
Conv2d output shape: torch.Size([1, 6, 28, 28])
Sigmoid output shape: torch.Size([1, 6, 28, 28])
AvgPool2d output shape: torch.Size([1, 6, 14, 14])
Conv2d output shape: torch.Size([1, 16, 10, 10])
Sigmoid output shape: torch.Size([1, 16, 10, 10])
AvgPool2d output shape: torch.Size([1, 16, 5, 5])
Flatten output shape: torch.Size([1, 400])
Linear output shape: torch.Size([1, 120])
Sigmoid output shape: torch.Size([1, 120])
Linear output shape: torch.Size([1, 84])
Sigmoid output shape: torch.Size([1, 84])
Linear output shape: torch.Size([1, 10])

batch_size=64
train_iter,test_iter=load_data_fashion_mnist(batch_size=batch_size)
#@save
def train_ch6(net,train_iter,test_iter,num_epochs,lr,device):
    #用GPU训练模型
    def init_weights(m):
        if type(m)==nn.Linear or type(m)==nn.Conv2d:
            nn.init.xavier_uniform_(m.weight)
    net.apply(init_weights)
    print('training on',device)
    net.to(device)
    optimizer=torch.optim.SGD(net.parameters(),lr)
    loss=nn.CrossEntropyLoss()
    animator=d2l.Animator(xlabel='epoch',xlim=[1,num_epochs],legend=['train loss','train acc','test acc'])
    timer,num_batches=d2l.Timer(),len(train_iter)
    for epoch in range(num_epochs):
        #训练损失之和,训练准确率之和,样本数
        metric=d2l.Accumulator(3)
        net.train()
        for i,(X,y) in enumerate(train_iter):
            timer.start()
            optimizer.zero_grad()
            X,y=X.to(device),y.to(device)
            y_hat=net(X)
            l=loss(y_hat,y)
            l.backward()
            optimizer.step()
            with torch.no_grad():
                #X.shape[0]是样本数
                metric.add(l*X.shape[0],d2l.accuracy(y_hat,y),X.shape[0])
            timer.stop()
            train_l=metric[0]/metric[2]
            train_acc=metric[1]/metric[2]
            if(i+1)%(num_batches//5)==0 or i==num_batches-1:
                animator.add(epoch+(i+1)/num_batches,(train_l,train_acc,None))
        test_acc=evaluate_accuracy_gpu(net,test_iter)
        animator.add(epoch+1,(None,None,test_acc))
    print(f"loss{train_l:.3f},train acc{train_acc:.3f},test acc {test_acc:.3f}")
    print(f"{metric[2]*num_epochs/timer.sum():.1f}examples/sec on device{device}")
    
lr,num_epochs=0.01,10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

注意:如果将激活函数从sigmoid换成ReLU时,需要将学习率调小,否则train_loss和train acc变化很小

二、AlexNet

1、组成:五个卷积层,三个最大汇聚层3*3,两个全连接隐藏层,一个全连接输出层
2、使用ReLU激活函数
3、使用了丢弃法,控制全连接层的模型复杂度------而LeNet使用的是权重衰退
4、为了进一步扩充数据,AlexNet在训练的时候增加了大量的图像增强数据,如翻转,裁剪,变色,使模型更强壮,更大的样本量有效的减少了过拟合
在这里插入图片描述

import torch
from torch  import nn
from d2l import torch as d2l
net=nn.Sequential(
    #这里我们使用一个11*11的更大窗口来捕捉对象--输入图像(3*224*224)
    #同时步幅为4,以减少输出的高度和宽度
    #输出通道数目远大于LeNet
    nn.Conv2d(1,96,kernel_size=11,stride=4,padding=1),nn.ReLU(),
    nn.MaxPool2d(kernel_size=3,stride=2),
    #减小卷积窗口,使用填充为2来使得输入与输出的高宽一致,且增大输出通道数
    nn.Conv2d(96,256,kernel_size=5,padding=2),nn.ReLU(),
    nn.MaxPool2d(kernel_size=3,stride=2),
    #使用三个连续的卷积层和较小的卷积窗口
    #除以最后的卷积层,输出通道的数量进一步增加
    #在前两个卷积层后,汇聚层不用于减少输入的高度和宽度
    nn.Conv2d(256,384,kernel_size=3,padding=1),nn.ReLU(),
    nn.Conv2d(384,384,kernel_size=3,padding=1),nn.ReLU(),
    nn.Conv2d(384,256,kernel_size=3,padding=1),nn.ReLU(),
    nn.MaxPool2d(kernel_size=3,stride=2),
    nn.Flatten(),
    #这里的全连接层的输出数量是LeNet中的好几倍,使用dropout层来减轻过拟合
    nn.Linear(6400,4096),nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(4096,4096),nn.ReLU(),
    nn.Dropout(p=0.5),
    #最后是输出层,由于这里使用的饿是Fashion-MNIST,所以类别数为10
    nn.Linear(4096,10))
X=torch.rand(1,1,224,224)
for layer in net:
    X=layer(X)
    print(layer.__class__.__name__,'output shape:\t',X.shape)

输出:
Conv2d output shape: torch.Size([1, 96, 54, 54])
ReLU output shape: torch.Size([1, 96, 54, 54])
MaxPool2d output shape: torch.Size([1, 96, 26, 26])
Conv2d output shape: torch.Size([1, 256, 26, 26])
ReLU output shape: torch.Size([1, 256, 26, 26])
MaxPool2d output shape: torch.Size([1, 256, 12, 12])
Conv2d output shape: torch.Size([1, 384, 12, 12])
ReLU output shape: torch.Size([1, 384, 12, 12])
Conv2d output shape: torch.Size([1, 384, 12, 12])
ReLU output shape: torch.Size([1, 384, 12, 12])
Conv2d output shape: torch.Size([1, 256, 12, 12])
ReLU output shape: torch.Size([1, 256, 12, 12])
MaxPool2d output shape: torch.Size([1, 256, 5, 5])
Flatten output shape: torch.Size([1, 6400])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 10])

三、使用块的网络(VGG)

经典卷积神经网络的基本组成部分是下面的这个序列:

  1. 带填充以保持分辨率的卷积层;
  2. 非线性激活函数,如ReLU;
  3. 汇聚层,如最大汇聚层。
    在这里插入图片描述VGG网络可以分为两部分:第⼀部分主要由卷积层和汇聚层组成,第⼆部分由全连接层组成。

#vgg_block--实现一个VGG块
#该函数有三个参数,分别对应于卷积层的数量num_conv,输入通道的数量in_channels和输出通道的数量out_channels
import torch 
from torch import nn
from d2l import torch as d2l
def vgg_block(num_convs,in_channels,out_channels):
    layers=[]
    for _ in range(num_convs):
        #经过卷积层图像的高宽不变
        layers.append(nn.Conv2d(in_channels,out_channels,kernel_size=3,padding=1))
        layers.append(nn.ReLU())
        in_channels=out_channels
    #经过池化层,高宽减半
    layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
    return nn.Sequential(*layers)

#原始VGG⽹络有5个卷积块,其中前两个块各有⼀个卷积层,后三个块各包含两个卷积层。第⼀个模块有64个
#输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到512。由于该⽹络使⽤8个卷积层和3个全连接层,因此它通常被称为VGG-11。
#有超参数变量conv_arch。该变量指定了每个VGG块⾥卷积层个数和输出通道数。
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))
def vgg(conv_arch):
    conv_blks=[]
    in_channels=1
    #卷积层部分
    for (num_convs,out_channels) in conv_arch:
        conv_blks.append(vgg_block(num_convs,in_channels,out_channels))
        in_channels=out_channels
     
    return nn.Sequential(
    *conv_blks,nn.Flatten(),
    #全连接层部分
    nn.Linear(out_channels*7*7,4096),nn.ReLU(),nn.Dropout(p=0.5),
    nn.Linear(4096,4096),nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(4096,10))

net=vgg(conv_arch)

X=torch.rand(size=(1,1,224,224))
for blk in net:
    X=blk(X)
    print(blk.__class__.__name__,'output shape:\t',X.shape)

输出:
Sequential output shape: torch.Size([1, 64, 112, 112])
Sequential output shape: torch.Size([1, 128, 56, 56])
Sequential output shape: torch.Size([1, 256, 28, 28])
Sequential output shape: torch.Size([1, 512, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
Flatten output shape: torch.Size([1, 25088])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 10])

ratio=4
#VGG-11计算量大  所以减少通道数来训练Fashion_MNIST
small_conv_arch=[(pair[0],pair[1]//ratio) for pair in conv_arch]  
net=vgg(small_conv_arch)
lr,num_epochs,batch_size=0.05,10,128
#注意这里要resize=224 Fashion_mnist中的原始图片28*28
train_iter,test_iter=load_data_fashion_mnist(batch_size,resize=224)
d2l.train_ch6(net,train_iter,test_iter,num_epochs,lr,d2l.try_gpu())

四、网络中的网络(NiN)

1、卷积层的输入和输出由四维张量组成,张量的每个轴分别对应样本、通道、⾼度和宽度。
全连接层的输⼊和输出通常是分别对应于样本和特征的二维张量。
2、 NiN的想法是在每个像素位置(针对每个⾼度和宽度)应用⼀个全连接层。如果我们将权重连接到每个空间位置,我们可以将其视为1 × 1卷积层,或作为在每个像素位置上独⽴作用的全连接层。从另⼀个角度看,即将空间维度中的每个像素视为单个样本,将通道维度视为不同特征(feature)。
NiN块以⼀个普通卷积层开始,后⾯是两个1 × 1的卷积层。这两个1 × 1卷积层充当带有ReLU激活函数的逐像素全连接层。
第⼀层的卷积窗口形状通常由用户设置。随后的卷积窗口形状固定为1 × 1。
3、 NiN和AlexNet的区别****是 NiN完全取消了全连接层,相反,NiN使用一个NiN块,其输出通道数等于标签类别数量,最后放一个平均汇聚层,生成一个对数几率。
注意:
1、NiN使用由⼀个卷积层和多个1 × 1卷积层组成的块。该块可以在卷积神经⽹络中使用,以允许更多的每像素非线性。
2、 NiN去除了容易造成过拟合的全连接层,将它们替换为全局平均汇聚层(即在所有位置上进行求和)。该汇聚层通道数量为所需的输出数量(例如,Fashion-MNIST的输出为10)。
3、 移除全连接层可减少过拟合,同时显著减少NiN的参数。
在这里插入图片描述

import torch
from torch import torch as d2l
from torch import nn
def nin_block(in_channels,out_channels,kernel_size,strides,padding):
    return nn.Sequential(
            nn.Conv2d(in_channels,out_channels,kernel_size,strides,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()
    )
net=nn.Sequential(
    #高宽224->54  (224-11+4)/4
    nin_block(1,96,kernel_size=11,strides=4,padding=0),
    #高宽减半52-》26
    nn.MaxPool2d(3,stride=2),
    #26-5+4+1->26 高宽不变 通道增加
    nin_block(96,256,kernel_size=5,strides=1,padding=2),
    #高宽减半 (26-3+2)/2->12
    nn.MaxPool2d(3,stride=2),
    #12-3+2+1=12
    nin_block(256,384,kernel_size=3,strides=1,padding=1),
    #(12-3+2)/2=5
    nn.MaxPool2d(3,stride=2),
    nn.Dropout(0.5),
    #标签类别数是10
    #(5-3+2+1)=5
    nin_block(384,10,kernel_size=3,strides=1,padding=1),
    # nn.AdaptiveAvgPool2d(output_size)
    #全局平均汇聚层(即在所有位置上进行求和)
    nn.AdaptiveAvgPool2d((1,1)),
    #将四维的输出转为二维的输出,其形状为(批量大小,10)
    nn.Flatten())

X=torch.rand(size=(1,1,224,224))
for layer in net:
    X=layer(X)
    print(layer.__class__.__name__,"output shape:",X.shape)

输出:
Sequential output shape: torch.Size([1, 96, 54, 54])
MaxPool2d output shape: torch.Size([1, 96, 26, 26])
Sequential output shape: torch.Size([1, 256, 26, 26])
MaxPool2d output shape: torch.Size([1, 256, 12, 12])
Sequential output shape: torch.Size([1, 384, 12, 12])
MaxPool2d output shape: torch.Size([1, 384, 5, 5])
Dropout output shape: torch.Size([1, 384, 5, 5])
Sequential output shape: torch.Size([1, 10, 5, 5])
AdaptiveAvgPool2d output shape: torch.Size([1, 10, 1, 1])
Flatten output shape: torch.Size([1, 10])

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

在GoogLeNet中,基本的卷积块被称为Inception块(Inception block)。
在这里插入图片描述

GoogLeNet⼀共使⽤9个Inception块和全局平均汇聚层的堆叠来生成其估计值。Inception块
之间的最大汇聚层可降低维度。第⼀个模块类似于AlexNet和LeNet,Inception块的组合从VGG继承,全局平均汇聚层避免了在最后使用全连接层。
在这里插入图片描述

#Inception块由四条并⾏路径组成。前三条路径使⽤窗⼝⼤⼩为1 × 1、3 × 3和5 × 5的卷积层,从不同空间⼤⼩中提取信息。
#中间的两条路径在输⼊上执⾏1 × 1卷积,以减少通道数,从⽽降低模型的复杂性。
#第四条路径使⽤3 × 3最⼤汇聚层,然后使⽤1 × 1卷积层来改变通道数。

#这四条路径都使⽤合适的填充来使输⼊与输出的⾼和宽⼀致,
#最后我们将每条线路的输出在通道维度上连结,并构成Inception块的输出。
#Inception块中,通常调整的超参数是每层输出通道数。
import torch 
from torch import nn
from d2l import torch as d2l 
from torch.nn import functional as F

class Inception(nn.Module):
    #c1-c4是每条路径的输出通道数
    def  __init__(self,in_channels,c1,c2,c3,c4,**kwargs):
        super(Inception,self).__init__(**kwargs)
        #线路一 单1*1卷积层
        self.p1_1=nn.Conv2d(in_channels,c1,kernel_size=1)
        #线路二 1*1卷积层后接3*3卷积层
        self.p2_1=nn.Conv2d(in_channels,c2[0],kernel_size=1)
        self.p2_2=nn.Conv2d(c2[0],c2[1],kernel_size=3,padding=1)
        #线路三 1*1卷积层后接5*5卷积层
        self.p3_1=nn.Conv2d(in_channels,c3[0],kernel_size=1)
        self.p3_2=nn.Conv2d(c3[0],c3[1],kernel_size=5,padding=2)
        #线路四 3*3的最大汇聚层 填充1 1*1的卷积层
        self.p4_1=nn.MaxPool2d(kernel_size=3,padding=1,stride=1)
        self.p4_2=nn.Conv2d(in_channels,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⼀共使⽤9个Inception块和全局平均汇聚层的堆叠来⽣成其估计值。
#Inception块之间的最⼤汇聚层可降低维度
#第⼀个模块类似于AlexNet和LeNet,Inception块的组合从VGG继承,全局平均汇聚层避免了在最后使⽤全连接层。

#第一个模块 使用64个通道,7*7卷积层
#如果输入是96  (96-7+2+6)/2=48  -> (48-3+2+2)/2=24        通道数64,高宽24*24
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))
#第二个模块使用两个卷积层
#24-》24-》(24-3+2+2)/2=12                           通道数192,高宽12*12
b2=nn.Sequential(nn.Conv2d(64,64,kernel_size=1),nn.ReLU(),
                nn.Conv2d(64,192,kernel_size=3,padding=1),
                nn.ReLU(),
                nn.MaxPool2d(kernel_size=3,stride=2,padding=1))

#第三个模块串联两个完整的Inception块
#第一个Inception块的输出通道数为64+128+32+32=256
#第二个Inception块的输出通道数为128+192+96+64=480   
#(12-3+2+2)/2=6                                       通道数 480  高宽6*6
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块,其输出通道数分别是是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。
#(6-3+2+2)/2=3                                       通道数832   高宽3*3
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))

#第五个模块 含输出通道数为256 + 320 + 128 + 128 = 832和384 + 384 + 128 + 128 = 1024的两个Inception块。
#第五模块的后⾯紧跟输出层,该模块同NiN⼀样使⽤全局平均汇聚层,将每个通道的⾼和宽变成1。最后我们将
#输出变成⼆维数组,再接上⼀个输出个数为标签类别数的全连接层
b5=nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                Inception(832, 384, (192, 384), (48, 128), 128),
                nn.AdaptiveAvgPool2d((1,1)),
                nn.Flatten())
                 
net=nn.Sequential(b1,b2,b3,b4,b5,nn.Linear(1024,10))
                 
X=torch.rand(size=(1,1,96,96))
for layer in net:
    X=layer(X)
    print(layer.__class__.__name__,"output shape:\t",X.shape)

Sequential output shape: torch.Size([1, 64, 24, 24])
Sequential output shape: torch.Size([1, 192, 12, 12])
Sequential output shape: torch.Size([1, 480, 6, 6])
Sequential output shape: torch.Size([1, 832, 3, 3])
Sequential output shape: torch.Size([1, 1024])
Linear output shape: torch.Size([1, 10])

总结:
1、Inception块相当于⼀个有4条路径的子网络。它通过不同窗口形状的卷积层和最大汇聚层来并行抽取信息,并使用1×1卷积层减少每像素级别上的通道维数从而降低模型复杂度。
2、 GoogLeNet将多个设计精细的Inception块与其他层(卷积层、全连接层)串联起来。其中Inception块的通道数分配之比是在ImageNet数据集上通过大量的实验得来的。

六、批量规范化

在这里插入图片描述
另外,批量规范化层在”训练模式“(通过⼩批量统计数据规范化)和“预测模式”(通过数据集统计规范化)中的功能不同。在训练过程中,我们无法得知使用整个数据集来估计平均值和方差,所以只能根据每个小批次的平均值和方差不断训练模型。而在预测模式下,可以根据整个数据集精确计算批量规范化所需的平均值和方差。

批量规范化--是在卷积层或全连接层之后,相应的激活函数之前应用的
import torch
from torch import nn
from d2l import torch as d2l

def batch_norm(X,gamma,beta,moving_mean,moving_var,eps,momentum):
    #通过is_grad_enabled来判断当前模式是训练模式还是预测模式
    if not torch.is_grad_enabled():
        #如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        X_hat=(X-moving_mean)/torch.sqrt(moving_var+eps)
    else:
        assert len(X.shape) in (2,4)
        if len(X.shape) ==2:
            #使用全连接层的情况下,计算特征维上的均值和方差
            #计算小批量样本在不同特征维上的均值和方差
            #(4,5)-》(1,5)
            mean=X.mean(dim=0)
            #方差=(X-均值)的平方求和/小批量样本数+小常量(避免除以0)
            var=((X-mean)**2).mean(dim=0)
        else:
            #使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差
            #这里我们要保持X的形状以便以后做广播运算
            #最后得到通道数个均值和方差
            mean=X.mean(dim=(0,2,3),keepdim=True)
            var=((X_mean)**2).mean(dim=(0,2,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
    #批量规范化还包括  拉伸参数(scale)γ和偏移参数(shift)β  γ*X+β才得到最终批量规范化的结果
    Y=gamma*X_hat+beta#缩放和移位
    return Y,moving_mean.data,moving_var.data
 
class BatchNorm(nn.Module):
    #num_features:完全连接层的输出数量或卷积层的输出通道数
    #num_dims:2表示完全连接层,4表示卷积层
    def __init__(self,num_features,num_dims):
        super.__init__()
        if num_dims==2:
            shape=(1,num_features)
        else:
            shape=(1,num_features,1,1)
        #参与求梯度和迭代的拉伸和偏移参数。分别初始化为1和0
        self.gamma=nn.Parameter(torch.ones(shape))
        self.beta=nn.Parameter(torch.zeros(shape))
        #非模型参数的变量初始化为0和1
        self.moving_mean=torch.zeros(shape)
        self.moving_var=torch.ones(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_varr.to(X.device)
        #保存更新过的moving_mean和moving_var
        Y,self.moving_mean,self.moving_var=batch_norm(X,self.gamma,self.beta,self.moving_mean,self.moving_var,eps=1e-5,momentum=0.9)
        return Y
          

七、ResNet

在这里插入图片描述

#ResNet沿⽤了VGG完整的3 × 3卷积层设计。残差块⾥⾸先有2个有相同输出通道数的3 × 3卷积层。
#每个卷积层后接⼀个批量规范化层和ReLU激活函数。
#然后我们通过跨层数据通路,跳过这2个卷积运算,将输⼊直接加在最后的ReLU激活函数前。
#这样的设计要求2个卷积层的输出与输⼊形状⼀样,从⽽使它们可以相加。
#如果想改变通道数,就需要引⼊⼀个额外的1 × 1卷积层来将输⼊变换成需要的形状后再做相加运算。
import torch
from torch import nn
from d2l import torch as d2l
from torch.nn import functional as F

class Residual(nn.Module):#@save
    def __init__(self,input_channels,num_channels,use_1x1conv=False,strides=1):
        super().__init__()
        self.conv1=nn.Conv2d(input_channels,num_channels,kernel_size=3,padding=1,stride=strides)
        self.conv2=nn.Conv2d(num_channels,num_channels,kernel_size=3,padding=1)
        if use_1x1conv:
            #1*1的卷积层是用来改变输入的形状的
            self.conv3=nn.Conv2d(input_channels,num_channels,kernel_size=1,stride=strides)
        else:
            self.conv3=None
        #卷积层num_features=输出通道数
        self.bn1=nn.BatchNorm2d(num_channels)
        self.bn2=nn.BatchNorm2d(num_channels)
    
    def forward(self,X):
        Y=F.relu(self.bn1(self.conv1(X)))
        Y=self.bn2(self.conv2(Y))
        #若输入输出形状一致,X和Y可以直接相加,如果不一致需要先改变X的形状
        if self.conv3:
            X=self.conv3(X)
        Y+=X
        return F.relu(Y)
    

此代码⽣成两种类型的⽹络:⼀种是当use_1x1conv=False时,应⽤ReLU⾮线性函数之前,将输⼊添加到输出。另⼀种是当use_1x1conv=True时,添加通过1 × 1卷积调整通道和分辨率。在这里插入图片描述
在这里插入图片描述

#输入和输出形状一致的情况
blk=Residual(3,3)
X=torch.rand(4,3,6,6)
Y=blk(X)
Y.shape

输出:torch.Size([4, 3, 6, 6])

#增加输出通道数的同时,减半输出的高和宽
blk=Residual(3,6,use_1x1conv=True,strides=2)
blk(X).shape

输出:torch.Size([4, 6, 3, 3])

b1=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))


#对第一个模块做了特别处理
#第⼀个模块的通道数同输⼊通道数⼀致。由于之前已经使⽤了步幅为2的最⼤汇聚层,所以⽆须减⼩⾼和宽
#一共四个resnet_block块,每个块里可以有若干个residual
def resnet_block(input_channels,num_channels,num_residuals,first_block=False):
    blk=[]
    for i in range(num_residuals):
        #如果不是第一个block,但是是第一个residual,那输入通道和输出通道可能不同
        #除了第一个块,其余的块都会在第一个residual经过一次高宽减半
        #如果是第一个block ,且是第一个residual  ->第⼀个模块的通道数同输⼊通道数⼀致
        #如果不是第一个residual ,那么经过第一个residual得到的输出通道数和最终的通道数都是相同的
        if i==0 and not first_block: #是第一个block中的第一个residual
            blk.append(Residual(input_channels,num_channels,use_1x1conv=True,strides=2))
        else:
            blk.append(Residual(num_channels,num_channels))
    return blk
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
#加入全局平均汇聚层,将高宽都变成1*1  即 批量大小*512*1*1
net = nn.Sequential(b1,b2,b3,b4,b5,nn.AdaptiveAvgPool2d((1,1)),nn.Flatten(),nn.Linear(512,10))
#在所有的架构中,分辨率降低(高宽减半),通道数增加,直到全局平均汇聚层聚集所有特征
X=torch.rand(size=(1,1,224,224))
for layer in net:
    X=layer(X)
    print(layer.__class__.__name__,"output shape :\t",X.shape)

Sequential output shape : torch.Size([1, 64, 56, 56])
Sequential output shape : torch.Size([1, 64, 56, 56])
Sequential output shape : torch.Size([1, 128, 28, 28])
Sequential output shape : torch.Size([1, 256, 14, 14])
Sequential output shape : torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape : torch.Size([1, 512, 1, 1])
Flatten output shape : torch.Size([1, 512])
Linear output shape : torch.Size([1, 10])

八、稠密连接⽹络(DenseNet)

在这里插入图片描述
ResNet和DenseNet的关键区别在于,DenseNet输出是连接(⽤图中的[, ]表⽰)⽽不是
如ResNet的简单相加。

DenseNet和ResNet
ResNet直接将输入与输出按通道维度相加,所以必须保证输出通道和输入通道一致且高宽一致,所以需要一个1*1的卷积核调整通道数和分辨率(调整分辨率是通过strides)
DenseNet不是简单相加,而是将卷积核的输入和输出在通道维上进行连接。
稠密网络主要由2部分构成:稠密块(dense block)和过渡层(transition layer)。前者定义如何连接输⼊和输出,⽽后者则控制通道数量,使其不会太复杂。

import torch
from torch import nn
from d2l import torch as d2l
def conv_block(input_channels,num_channels):
    return nn.Sequential(
    nn.BatchNorm2d(input_channels),nn.ReLU(),
    nn.Conv2d(input_channels,num_channels,kernel_size=3,padding=1))

#⼀个稠密块由多个卷积块组成,每个卷积块使用相同数量的输出通道。
#然⽽,在前向传播中,我们将每个卷积块的输⼊和输出在通道维上连结。
class DenseBlock(nn.Module):
    def __init__(self,num_convs,input_channels,num_channels):
        super(DenseBlock,self).__init__()
        layer=[]
        for i in range(num_convs):
            #输入通道维为num_channels*i+input_channels,因为每经过一个卷积块都要将输入和输出连接出来
            layer.append(conv_block(num_channels*i+input_channels,num_channels))
            self.net=nn.Sequential(*layer)
    
    def forward(self,X):
        for blk in self.net:
            Y=blk(X)
            #连接通道维度上每个块的输入和输出
            X=torch.cat((X,Y),dim=1)
        return X 
blk=DenseBlock(2,3,10)
X=torch.randn(4,3,8,8)
Y=blk(X)
Y.shape

torch.Size([4, 23, 8, 8])

#由于每个稠密块都会带来通道数的增加,使用过多则会过于复杂化模型。⽽过渡层可以⽤来控制模型复杂度。
#它通过1 × 1卷积层来减⼩通道数,并使⽤步幅为2的平均汇聚层减半⾼和宽,从⽽进⼀步降低模型复杂度。
def transition_block(input_channels,num_channels):
    return nn.Sequential(nn.BatchNorm2d(input_channels),nn.ReLU(),
                         #1*1卷积层用来减少通道数,不改变高宽
                         nn.Conv2d(input_channels,num_channels,kernel_size=1),
                         #平均汇聚层不改变通道高,减半高和宽
                         nn.AvgPool2d(kernel_size=2,stride=2))
                         
#DenseNet⾸先使用同ResNet⼀样的单卷积层和最⼤汇聚层。
b1=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))


#类似于ResNet使⽤的4个残差块,DenseNet使⽤的是4个稠密块。与ResNet类似,我们可以设置每个稠密块使⽤多少个卷积层。
#这⾥我们设成4,从⽽与 7.6节的ResNet-18保持⼀致。
#稠密块⾥的卷积层通道数(即增⻓率)设为32,所以每个稠密块将增加128个通道。
#在每个模块之间,ResNet通过步幅为2的残差块(Residual块中的第一个卷积层的strides)减⼩⾼和宽
#DenseNet则使用过渡层来减半⾼和宽,并减半通道数。
# num_channels为当前的通道数


#这里的增加率就相当于dense_block中的num_channels
num_channels,growth_rate=64,32
#共有四个dense_block,每个dense_block中都有四个卷积块
num_convs_in_dense_blocks=[4,4,4,4]
blks=[]
for i,num_convs in enumerate(num_convs_in_dense_blocks):
    blks.append(DenseBlock(num_convs,num_channels,growth_rate))
    #上一个稠密块的输出通道数
    num_channels+=growth_rate*num_convs
    #在稠密块之间添加一个转换层,使通道数量减半
    if i!=len(num_convs_in_dense_blocks)-1:
        blks.append(transition_block(num_channels,num_channels//2))
        num_channels=num_channels//2

#与ResNet类似,最后接上全局汇聚层和全连接层来输出结果。
net=nn.Sequential(b1,*blks,
                  nn.BatchNorm2d(num_channels),nn.ReLU(),
                  nn.AdaptiveAvgPool2d((1,1)),
                  nn.Flatten(),
                  nn.Linear(num_channels,10))

输出:
Sequential shape: torch.Size([1, 64, 32, 32])
DenseBlock shape: torch.Size([1, 192, 32, 32])
Sequential shape: torch.Size([1, 96, 16, 16])
DenseBlock shape: torch.Size([1, 224, 16, 16])
Sequential shape: torch.Size([1, 112, 8, 8])
DenseBlock shape: torch.Size([1, 240, 8, 8])
Sequential shape: torch.Size([1, 120, 4, 4])
DenseBlock shape: torch.Size([1, 248, 4, 4])
BatchNorm2d shape: torch.Size([1, 248, 4, 4])
ReLU shape: torch.Size([1, 248, 4, 4])
AdaptiveAvgPool2d shape: torch.Size([1, 248, 1, 1])
Flatten shape: torch.Size([1, 248])
Linear shape: torch.Size([1, 10])

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值