pytorch系统学习

经过网络后的输出图片形状的计算公式:

  1. 第一种情况:如果stride值为0的话,输入形状是n_h x n_w,卷积核窗口是k_h x k_w,那么输出形状是(n_h - k_h +1) x (n_w - k_w +1)。如果这时有p_h值的话,那么公式就是(n_h - k_h +2×p_h+1) x (n_w - k_w+2 * p_w +1)。
  2. 第二种情况:如果stride值不为0的话,下面的计算公式中如果有padding=(2,2)的话,那么p_h=p_w=4,因为对于w而言,是左边增加padding值2且右边对称位置也是增加padding值2,所以总的p_w=4。当在高上步幅为s_h时,在宽上步幅为s_w时,输出形状为:[floor(n_h - k_h + p_h +s_h )/ s_h] x [floor(n_w - k_w + p_w + s_w)/s_w](floor表示向下取整)
  3. 小tip:如果步幅为s,填充为s/2,假设s/2为整数,卷积核的高和宽为2s,转置卷积核将输入的高和宽分别放大s倍。

自定义层

  1. 使用Module自定义层,从而可以被重复调用。
不含有模型参数的自定义层
  1. 下面例子中,CenteredLayer类通过继承Module类自定义了一个将输入减掉均值后输出的层,并且将层的计算定义在了forward函数里面,这个层里不含有模型参数。
import torch
import torchvision
from torchvision import models
from torch import nn

class CenteredLayer(nn.Module):
    def __init__(self,**kwargs):
        super(CenteredLayer,self).__init__(**kwargs)
    def forward(self,x):
        return x - x.mean()
  1. 第二步,实例化这个层,然后做前向计算。
layer = CenteredLayer()
layer(torch.tensor([1,2,3,4,5],dtype=torch.float))
  1. 第三步,用它来构造更复杂的模型。
net = nn.Sequential(nn.Linear(8,128),CenteredLayer())
  1. 第四步,打印自定义层各个输出的均值,因为均值是浮点型,所以它的值是一个很接近0的数值。
y = net(torch.rand(4,8))
y.mean().item()
含模型参数的自定义层
  1. 可以自定义含有模型参数的自定义层,其中模型参数是可以通过训练来学到的。
  2. Parameter类是Tensor的子类,如果一个Tensor是Parameter,那么它会自动被添加到模型的参数列表里,所以在自定义含有参数模型的层时,应该将参数定义为Parameter。除了直接定义成Parameter类外,还可以使用ParameterList和ParameterDict分别定义参数的列表和字典。
  3. ParameterList接收一个Parameter实例的列表作为输入,然后得到一个参数列表,使用的时候可以用索引来访问某个参数,另外也可以使用append和extend在列表后面新增参数。
class MyDense(nn.Module):
    def __init__(self):
        super(MyDense,self).__init__()
        self.params = nn.ParameterList([nn.Parameter(torch.randn(4,4)) for i in range(3)])
        self.params.append(nn.Parameter(torch.randn(4,1)))
    def forward(self,x):
        for i in range(len(self.params)):
            x = torch.mm(x,self.params[i])
        return x
net = MyDense()
print(net)
  1. ParameterDict接收一个Parameter实例的字典作为输入然后得到一个参数字典,然后可以按照字典的规则使用。使用update()新增参数,使用keys()返回所有键值,使用items()返回所有键值对等。
class MyDictDense(nn.Module):
    def __init__(self):
        super(MyDictDense,self).__init__()
        self.params = nn.ParameterDict({
            'linear1':nn.Parameter(torvh.randn(4,4)),
            'linear2':nn.Parameter(torch.randn(4,1))
        })
        self.params.update({'linear3':nn.Parameter(torch.randn(4,2))})
    def forward(self,x,choice='linear1'):
        return torch.mm(x,self.params[choice])
net = MyDictDense()
print(net)
  1. 然后就可以利用传入的键值来进行不同的前向传播。
x = torch.ones(1,4)
print(net(x,'linear1'))
print(net(x,'linear2'))
print(net(x,'linear3'))
  1. 也可以使用自定义层构造模型,和pytorch的其他层在使用上是一样的。
net = nn.Sequential(
    MyDictDense(),
    MyDense(),
)
print(net)
print(net(x))

读取和存储

  1. 需要把训练好的模型部署到很多不同的设备。这时可以把内存中训练好的模型参数存储在硬盘上供后续使用。
读写Tensor
  1. 可以直接使用save函数和load函数分别存储和读取Tensor,save使用python的pickle实用程序将对象序列化,然后将序列化的对象保存到disk。使用save可以保存各种对象,包括模型、张量和字典等。而load使用pickle unpickle工具将pickle的对象文件反序列化为内存。
  2. 下面的例子创建了Tensor变量x,并且将它存储在x.pt文件里面。
x=torch.ones(3)
torch.save(x,'x.pt')
  1. 第二步将数据从存储的文件读回内存。
x2 = torch.load('x.pt')
print(x2)
state_dict
  1. Module的可学习参数,即权重和偏差,通过model.parameters()访问,state_dict是一个从参数名称影射到参数Tensor的字典对象。
class MLP(nn.Module):
    def __init__(self):
        super(MLP,self).__init__()
        self.hidden = nn.Linear(3,2)
        self.act = nn.ReLU()
        self.output = nn.Linear(2,1)
    def forward(self,x):
        a = self.act(self.hidden(x))
        return self.output(a)
net = MLP()
net.state_dict()
  1. 只有具有可学习参数的层,如卷积层、线性层等才有state_dict中的条目,优化器optim也有一个state_dict,其中包含优化器状态以及所使用到的超参数的信息。
optimizer = torch.optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
optimizer.state_dict()
保存和加载模型
  1. pytorch中保存和加载训练模型有两种方式:
  2. 方式一:仅仅保存和加载模型参数state_dict
  3. 方式二:保存和加载整个模型
方式一:保存和加载state_dict

保存:

torch.save(model.state_dict(),PATH)#推荐的文件后缀是pt或者pth

加载

model = TheModelClass(*args,**kwargs)
model.load_state_dict(torch.load(PATH))
保存和加载整个模型

保存

torch.save(model,PATH)

加载

model = torch.load(PATH)
总结
  1. 利用save和load函数可以很方便的读写Tensor
  2. 通过save函数和load_state_dict函数可以方便读写模型的参数。

GPU计算

  1. 通过nvidia-smi命令查看显卡信息。
  2. 默认下,pytorch会将数据创建在内存,然后利用cpu来计算。
  3. 使用.cuda()可以将CPU上的Tensor转换(复制)到GPU上,用.cuda(i)来表示第i块GPU及相应的显存。
  4. 用Tensor的.device属性来查看该Tensor所在的设备。
  5. 如果是对在GPU上的数据进行计算,那么结果还是存放在GPU上。
  6. 存储在不同位置上的数据是不可以直接计算的,即存放在CPU上的数据不可以直接与存放在GPU上的数据运算,位于不同GPU上的数据也是不能直接计算的。
  7. 和Tensor类似,模型也是可以通过.cuda转换到GPU上。
  8. 而且也是要保证模型输入的Tensor和模型都在同一个设备上。

二维卷积层

  1. 虽然卷积层得名于卷积运算,但是在卷积层中使用更加直观的互相关运算,二维卷积层中,一个二维输入数组和一个二维核数组通过互相关运算输出一个二维数组。
  2. 二维互相关运算:卷积窗口从输入数组的最左上方开始,按照从左往右、从上往下的顺序,依次在输入数组上滑动,当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按照元素相乘并且求和,得到输出数组中相应位置的元素。
  3. 二维卷积层将输入和卷积核做互相关运算,并且加上一个标量偏差得到输出,卷积层的模型参数包括了卷积核和偏差。在训练模型的时候,通常先对卷积核随机初始化,然后不断迭代卷积核和偏差。
  4. 下面是一个卷积层的简单应用,检测图像中物体的边缘,即找到像素变化的位置。
  5. 第一步:首先构造一张高为6像素和宽为8像素的图像。图像的中间4列是黑(值为0),其余为白(值为1)。
x = torch.ones(6,8)
x[:,2:6] = 0#表示所有行,第2列到第五列,不包括第6列
print(x)

  1. 第二步:构造一个高和宽为1和2的卷积核K。当它与输入做互相关运算。
k = torch.tensor([[1,-1]])#所以是1行2列
  1. 第三步:将输入x和设计的卷积核k做互相关运算,将从白到黑的边缘和从黑到白的边缘分别检测为了1和-1,其余部分的输出为0
x = torch.ones(6,8)
x[:,2:6] = 0
# print(x)
k = torch.tensor([[1,-1]],dtype=torch.float)
def corr2d(x,k):
    h,w = k.shape
    y = torch.zeros((x.shape[0]-h+1,x.shape[1]-w+1))
    for i in range(y.shape[0]):
        for j in range(y.shape[1]):
            y[i,j]=(x[i:i+h,j:j+w]*k).sum()
    return y

y = corr2d(x,k)
print(y)

  1. 由上面这个卷积核的简单应用例子可以看出:卷积层可以通过重复使用卷积核有效的表征局部空间。
  2. 下面再举一个例子,是通过数据来学习核数组。它使用物体边缘检测中的输入数据x和输出数据y来学习我们构造的核数组k。
  3. 第一步:首先构造一个卷积层,其卷积核将被初始化成随机数组。接下来在每一次迭代中,使用平方误差来比较y和卷积层的输出,计算梯度来更新权重。
  4. 在进行第一步之前补充的一个准备操作:基于corr2d函数来实现一个自定义的二维卷积层。在构造函数__init__里,声明weight和bias两个模型参数。前向计算函数forward则直接调用corr2d函数再加上偏差。
#补充操作里的实现代码
class Conv2D(nn.Module):
    def __init__(self,kernel_size):
        super(Conv2D,self).__init__()
        self.weight = nn.Parameter(torch.randn(kernel_size))
        self.bias = nn.Parameter(torch.randn(1))
    def forward(self,x):
        return corr2d(x,self.weight)+self.bias
#下面是第一步操作中的实现代码
#构造一个核数组形状是(1,2)的二维卷积层
conv2D = Conv2D(kernel_size=(1,2))
step = 20
lr = 0.01
for i in range(step):
    y_hat = conv2D(x)
    l = ((y_hat-y)**2).sum()
    l.backward()
    #梯度下降
    conv2D.weight.data -= lr*conv2D.weight.grad
    conv2D.bias.data -= lr*conv2D.bias.grad
    #梯度清零
    conv2D.weight.grad.fill_(0)
    conv2D.bias.grad.fill_(0)
    if (i+1)%5 == 0:
        print('Step %d,loss %.3f' %(i+1,l.item()))
  1. 第二步:看一下学习到的卷积核的参数
print('weight:',conv2D.weight.data)
print('bias:',conv2D.bias.data)
  1. 可以看到学习到的卷积核的权重参数与之前在第一个例子识别图像边缘例子中定义的核数组k很接近,学习到的偏置参数接近0。
互相关运算与卷积运算
  1. 卷积运算与互相关运算类似,为了得到卷积运算的输出,只需要将核数组左右翻转并且上下翻转。再与输入数组做互相关运算。
  2. 深度学习中,核数组都是学习出来的,卷积层无论使用互相关运算或者是卷积运算都不影响模型预测时的输出。所以卷积层能使用互相关运算替代卷积运算。
特征图与感受野
  1. 二维卷积层输出的二维数组可以看做是输入在空间维度(宽和高)上某一级的表征,也叫特征图。
  2. 影响元素x的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做x的感受野。
  3. 可以通过更深的卷积神经网络使得特征图中单个元素的感受野变得广阔,从而捕捉输入上更大尺寸的特征。
填充和歩幅
  1. 之前讲的例子中利用3x3的输入图片和2x2的卷积核得到了2x2的输出。
  2. 规律是:输入形状是n_h x n_w,卷积核窗口是k_h x k_w,那么输出形状是(n_h - k_h +1) x (n_w - k_w +1)。
  3. 填充padding指的是在高和宽的两侧填充元素,一般是元素0。
  4. 一般情况下设置p_h=k_h -1和p_w = k_w -1来使得输入和输出具有相同的高和宽。这样会方便在构造网络时推测每个层的输出形状。
  5. 卷积神经网络经常使用奇数高宽的卷积核,如1/3/5/7等。
  6. 如果卷积核在输入图像上滑动时,如果输入图像的元素无法填满窗口,那么是没有结果输出的。
  7. 规律:下面的计算公式中如果有padding=(2,2)的话,那么p_h=p_w=4,因为对于w而言,是左边增加padding值2且右边对称位置也是增加padding值2,所以总的p_w=4。当在高上步幅为s_h时,在宽上步幅为s_w时,输出形状为:[floor(n_h - k_h + p_h +s_h )/ s_h] x [floor(n_w - k_w + p_w + s_w)/s_w],上面的规律公式中,如果有:输入图像的高和宽能分别被高和宽上的步幅整除的话,且有p_h = k_h -1和p_w = k_w -1,那么输出形状是(n_h/s_h) x (n_w/s_w)。
  8. 上面公式中的n_h表示输入图片的高,k_h表示卷积核的高,p表示picture,k表示kernel。p_h表示在高上的填充padding值。s_h表示在高上的步幅值。
  9. 填充可以增加输出的高和宽,这常用来使得输出与输入具有相同的高和宽。
  10. 步幅可以减小输出的高和宽,例如输出的高和宽仅仅为输入的高和宽的1/n,n为大于1的整数。

多输入通道和多输出通道

  1. 彩色图像在高和宽2个维度外还有RGB 3个颜色通道。所以彩色图片可以表示成3 x h x w的多维数组。将值为3的这一维称为通道维。
  2. 当输入数据含有多通道时,需要构造一个输入通道数与输入数据相同的卷积核,从而能够与含有多通道的输入数据做互相关运算。
  3. 由于输入图像和卷积核都有c_i个通道,c表示channel。可以在各个通道上对输入的二维数组和卷积核的二维核数组做互相关运算。再将这c_i个互相关运算的二维输出按照通道相加,得到一个二维数组,即为含有多个通道的输入数据与多输入通道的卷积核做二维互相关运算的输出。
  4. 当输入通道有多个时,因为对各个通道的结果做了累加,所以不论输入通道数是多少,输出通道数总是1。
  5. 对于多输出通道,其中第一个通道的结果与之前输入数组x与多输入通道、单输出通道核的计算结果一致。
  6. 如果希望得到含有多个通道的输出,那么为每个输出通道分别创建核数组,将它们在输出通道维上联结。在做互相关运算时,每个输出通道上的结果由卷积核在该输出通道上的核数组与整个输入数组计算而来。
1 x 1卷积核
  1. 窗口形状为1x1的多通道卷积层,称为1x1卷积层。因为使用了最小窗口,1x1卷积失去了卷积层可以识别高和宽维度上相邻元素构成的模式的功能。1x1卷积的主要计算发生在通道维上。输入和输出具有相同的高和宽,输出中的每个元素来自输入中在高和宽上相同位置的元素在不同通道间的按权重累加。假设将通道维当做特征维,将高和宽维度上的元素当做数据样本,那么1x1卷积层的作用与全连接层等价。
  2. 在很多模型里将1x1卷积层当做保持高和宽维度形状不变的全连接层使用,可以通过调整网络层间的通道数来控制模复杂度。也就是说1x1卷积层通常用来调整网络层之间的通道数,并且控制模型的复杂度。
  3. 使用多通道可以扩展卷积层的模型参数。

池化层

  1. 在二维卷积层里介绍的图像物体边缘检测应用中,构造卷积核从而精确地找到了像素变化的位置。设任意二维数组x的i行j列的元素为X[i,j],如果构造的卷积核输出Y[i,j]=1,那么说明输出中X[i,j]和X[i,j+1]数值不一样。也就是可能意味着物体边缘通过这两个元素之间,但是实际图像里,感兴趣的物体不会总是出现在固定位置上,即使连续拍摄同一个物体也极有可能出现像素位置上的偏移。这就会导致同一个边缘对应的输出可能出现在卷积输出Y中的不同位置,进而对后面的模式识别造成不便。
  2. 池化层pooling的作用就是为了缓解卷积层对位置的过度敏感性。
  3. 二维最大池化层和平均池化层。同卷积层一样,池化层对输入数据的一个固定窗口中的元素计算输出,不同于卷积层里计算输入和核的互相关性,池化层直接计算池化窗口内元素的最大值或者平均值。
  4. 再次回到上面提到的例子,将卷积层的输出作为2x2最大池化层的输入,设该卷积层输入是X,池化层输出为Y,无论是X[i,j]和X[i,j+1]值不同,还是X[i,j+1]和X[i,j+2]值不同,池化层输出均有Y[i,j]=1,也就是说,使用2x2最大池化层时,只要卷积层识别的模式在高和宽上移动不超过一个元素,依然可以将它检测出来。
  5. 同卷积层一样,池化层也可以在输入的高和宽两侧的填充并调整窗口的移动步幅来改变输出形状。
  6. 在处理多通道输入数据时,池化层对每个通道分别池化,而不是像卷积层那样将各通道的输入按照通道相加,这意味着池化层的输出通道数与输入通道数一样。

卷积神经网络LeNet

  1. 一个简单的例子:构造一个含有单隐藏层的多层感知机模型对数据集中的图像分类,每张图像高和宽都是28像素,将图像中的像素逐行进行展开,得到长度为784的向量,并且输入到全连接层,但是这种分类方法具有局限性。
    (1)局限性一:图像在同一列邻近的像素在这个向量中可能相距较远,它们构成的模式可能难以被模型识别。
    (2)局限二:对于大尺寸的输入图像,使用全连接层容易造成模型过大。
  2. 对于上面的两个局限性,卷积层尝试解决这两个问题,一方面,卷积层保留输入形状,使得图像的像素在高和宽两个方向上的相关性均可以被有效识别。另一方面,卷积层通过滑动窗口将同一卷积核与不同位置的输入重复计算,从而避免参数尺寸过大。
  3. LeNet是最早期的卷积神经网络,LeNet分为卷积层块和全连接层块两部分。
  4. 卷积层块的基本单位是卷积层后接最大池化层,卷积层用来识别图像里的空间模式,如线条和物体局部,之后的最大池化层则用来降低卷积层对位置的敏感性。卷积层块由两个这样的基本单位重复堆叠形成。
  5. 在卷积层块中,每个卷积都使用5x5的窗口,并在输出上使用sigmoid激活函数。第一个卷积层输出通道是6个。第二个卷积层输出通道数是16个。
  6. 这是因为第二个卷积层比第一个卷积层的输入的高和宽要小,所以增加输出通道使得两个卷积层的参数尺寸类似。
  7. 卷积层块的两个最大池化层的窗口形状均为2x2,且步幅为2,由于池化窗口与步幅形状相同,池化窗口在输入上每次滑动所覆盖的区域并不重叠。
  8. 卷积层块的输出形状为×批量大小,通道,高,宽)。当卷积层块的输出传入全连接层块时,全连接层块会将小批量中每个样本变平。即全连接层的输入形状会变成二维,其中第一维是小批量中的样本,第二维是每个样本变平后的向量表示,且向量长度为通道、高、宽的乘积。
  9. 全连接层块含有3个全连接层,它们输出个数分别为120、84、10.其中10是输出的类别个数。
  10. 神经网络可以直接基于图像的原始像素进行分类,这种称为端到端的方法可以节省很多中间步骤。
  11. 使用较干净的数据集和较有效的特征甚至比机器学习模型的选择对图像分类结果影响更大。
  12. 输入的逐级表示由多层模型中的参数决定,而这些参数都是学习出来的。
AlexNet
  1. AlexNet使用了8层卷积神经网络。它首次证明了学习到的特征可以超越手工设计的特征。
  2. AlexNet与LeNet的设计理念很相似,但是也有显著区别。
  3. 区别一:与相对较小的LeNet相比,AlexNet包含8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。下面详细介绍这些层的设计。
    (1)AlexNet第一层中的卷积窗口形状是11 x 11,因为ImageNet中绝大多数图像的高和宽均比MNIST图像的高和宽大10倍以上,ImageNet图像的物体占用更多像素,所以需要更大的卷积窗口来捕获物体。
    (2)第二层中的卷积窗口形状减小到5x5,之后全部采用3x3卷积核。
    (3)第一、第二和第五个卷积层之后都是用了窗口形状为3x3、步幅为2的最大池化层。
    (4)AlexNet使用的卷积通道数也大于LeNet中的卷积通道数数十倍。
    (5)接着最后一个卷积层的是两个输出个数为4096的全连接层。这两个巨大的全连接层带来了将近1GB的模型参数。
  4. 区别二:AlexNet将sigmoid激活函数改成了更加简单的ReLU激活函数。原因有以下两个:
    (1)原因一:ReLU激活函数的计算更加简单。例如它没有sigmoid激活函数中的求幂运算。
    (2)原因二:ReLU激活函数在不同的参数初始化方法下使得模型更容易训练。这是由于当sigmoid激活函数输出接近0或者1时,这些区域的梯度几乎为0,从而造成反向传播无法继续更新部分模型参数,而ReLU激活函数在正区间的梯度恒为1。因此若模型参数初始化不当,sigmoid函数极有可能在正区间得到几乎为0的梯度,从而令模型无法得到有效训练。
  5. 区别三:AlexNet通过丢弃法来控制全连接层的模型复杂度,而LeNet没有使用丢弃法。
  6. 区别四:AlexNet引入了大量的图像增广,如翻转、裁剪和颜色变化,从而扩大数据集来缓解过拟合。
  7. AlexNet网络的输入图像为224x224
  8. AlexNet跟LeNet结构类似,但是使用了更多的卷积层和更大的参数空间来拟合大规模数据集,AlexNet是浅层神经网络和深层神经网络的分界线。

使用重复元素的网络VGG

  1. AlexNet在LeNet的基础上增加了3个卷积层,但是AlexNet网络在网络的卷积窗口、输出通道数目、构造顺序上都做了调整。
  2. AlexNet指明了深度卷积网络可以取得较好的结果,但是没有提供简单的规则指导后来研究者如何设计新网络。
  3. VGG提出了可以通过重复使用简单的基础块来构建深度模型的思路。
  4. VGG块的组成规律是:连续使用数个相同的填充为1、窗口形状为3的卷积层后接上一个步幅为2、窗口形状为2的最大池化层。卷积层保持输入的高和宽不变,而池化层则对其减半。
  5. 对于给定的感受野(与输出有关的输入图片的局部大小),采用堆积的小卷积核优于采用大的卷积核,因为可以增加网络深度来保证学习更复杂的模式,而且代价还比较小(参数更少)。在VGG中,使用3个3x3的卷积核来代替7x7卷积核,使用2个3x3卷积核来代替5x5卷积核,这样做的目的是在保证具有相同感受野的条件下,提升了网络的深度,一定程度上提升了神经网络的效果。
  6. 与AlexNet和LeNet一样,VGG网络由卷积层模块后接全连接层模块构成。
  7. 卷积层模块中串接数个vgg_block,其超参数由变量conv_arch定义。该变量指定了每个VGG块里卷积层个数和输入输出通道数。全连接模块和AlexNet一样。
  8. 下面构造一个VGG网络,有5个卷积块,前2块使用单卷积层,后3块使用双卷积层。第一块的输入输出通道分别为1(因为后面要使用的Fashion-MNIST数据的通道数为1)和64,之后每次对输出通道数翻倍,直到变成512.因为这个网络使用了8个卷积层和3个全连接层,所以被叫做VGG-11。
import time
import torch
from torch import nn,optim

import sys
from d2lzh_pytorch import *
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

#定义vgg块
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))#这里会使得宽高减半
    return nn.Sequential(*blk)

#构造VGG-11
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#取任意值

#实现VGG-11
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):
        #每经过一个vgg_block都会使得宽高减半
        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_ubits,10)
                                      ))
    return net

#下面构造一个高宽都是224的单通道数据样本来观察每一层的输出形状
net = vgg(conv_arch,fc_features,fc_hidden_units)
X = torch.rand(1,1,224,224)
#named_children获取一级子模块及其名字,names_modules会返回所有子模块,包括子模块的子模块
for name,blk in net.named_children():
    X = blk()
    print(name,'output shape:',X.shape)

'''
输出结果如下:
vgg_block_1 output shape:torch.Size([1,64,112,112])
vgg_block_2 output shape:torch.Size([1,128,56,56])
vgg_block_3 output shape:torch.Size([1,256,28,28])
vgg_block_5 output shape:torch.Size([1,512,7,7])
fc output shape:torch.Size([1,10])
每次将输入的高和宽减半,直到最终高和宽变成7后传入全连接层。
与此同时,输出通道数每次翻倍,直到变成512.因为每个卷积层的窗口大小一样。
所以每层的模型参数尺寸和计算复杂度与输入高、输入宽、输入通道数和输出通道数的乘积成正比。
VGG这种高和宽减半以及通道翻倍的设计使得多数卷积层都有相同的模型参数尺寸和计算复杂度。
'''

网络中的网络NiN

  1. LeNet AlexNet VGG在设计上的共同之处在于:先是以卷积层构成的模块充分抽取空间特征,再以全连接层构成的模块来输出分类结果。
  2. AlexNet和VGG对LeNet的改进在于如何对这两个模块加宽(增加通道数)和加深。
  3. NiN提出另一种思路:即串联多个由卷积层和全连接层构成的小网络来构建一个深层网络。
  4. 卷积层的输入和输出通常是四位数组 (样本数目、通道数目、高、宽 ),而全连接层的输入和输出通常是二维数组(样本数目、特征数目)。
  5. 如果要在全连接层后接上卷积层,那么需要将全连接层的输出变化成四维。
  6. 1x1卷积层可以看成全连接层,其中空间维度(高和宽)上的每个元素相当于样本,通道相当于特征。因此NiN使用1x1卷积层来替代全连接层,从而使得空间信息能自然地传递到后面的层中。
  7. NiN块是NiN中的基础块,它由一个卷积层加两个充当全连接层的1x1卷积层串联而成。其中第一个卷积层的超参数可以自行设置,第二和第三个卷积层的超参数一般是固定的。
import time
import torch
from torch import nn,optim
import sys
from d2lzh_pytorch import *
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

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_szie=1),
                        nn.ReLU(),
                        nn.Conv2d(out_channels,out_channels,kernel_size=1),
                        nn.ReLU())
    return blk

  1. NiN与AlexNet的卷积层设定有类似的地方:NiN使用卷积窗口形状分别为11x11和5x5和3x3的卷积层,相应的输出通道数也与AlexNet中的一致。每个NiN块后接一个步幅为2、窗口形状为3x3的最大池化层。
  2. 除了使用NiN块以后,NiN还有一个设计与AlexNet显著不同:NiN去掉AlexNet最后的3个全连接层,取而代之的是NiN使用输出通道数等于标签类别数的NiN块,然后使用全局平均池化层对每个通道中所有元素平均并直接用于分类。这里的全局平均池化层即窗口形状等于输入空间维形状的平均池化层。NiN的这个设计的好处是可以显著减小模型参数尺寸,从而缓解过拟合。但是这个设计有时会造成获得有效模型的训练时间增加。
import time
import torch
from torch import nn,optim
import sys
from d2lzh_pytorch import *
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

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_szie=1),
                        nn.ReLU(),
                        nn.Conv2d(out_channels,out_channels,kernel_size=1),
                        nn.ReLU())
    return blk

class GloabalAvgPool2d(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)
'''
输出结果是:
0 output shape:torch.Size([1,96,54,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])
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])
'''
  1. NiN重复使用由卷积层和代替全连接层的1x1卷积层构成的NiN块来构建深层网络。
  2. NiN去除了容易造成过拟合的全连接输出层,而是将其替换为输出通道数等于标签类别数的NiN块和全局平均池化层。
  3. NiN的以上设计思想影响了后面一系列卷积神经网络的设计。

含并行联结的网络GoogLeNet

  1. GoogLeNet吸收了NiN中网络串联网络的思想。
  2. GoogLeNet中的基础卷积块叫做Inception块。Inception块里有4条并行的线路,前3条线路使用的窗口大小分别是1x1,3x3,5x5的卷积层来抽取不同空间尺寸下的信息。其中中间2个线路会对输入先做1x1卷积来减少输入通道数,以降低模型复杂度、第4条线路使用3x3最大池化层,后接1x1卷积层来改变通道数。4条线路都使用了合适的填充来使得输入与输出的高和宽一致。最后将每条线路的输出在通道维上连接,并输入到接下来的层。
  3. Inception块中可以自定义的超参数是每个层的输出通道数,以此来控制模型复杂度。
import time
import torch
from torch import nn,optim
import torch.nn.functional as F
import sys
from d2lzh_pytorch import *
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class Inception(nn.Module):
    #c1 c2 c3 c4是每条线路里的输出通道数
    def __init__(self,in_c,c1,c2,c3,c4):
        super(Inception,self).__init__()
        #线路1,单1x1卷积层
        self.p1_1 = nn.Conv2d(in_c,c1,kernel_size=1)
        #线路2,1x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_c,c2[0],kernrl_size=1)
        self.p2_2 = nn.Conv2d(c2[0],c2[1],kernrl_size=3,padding=1)
        #线路3,1x1卷积层后接5x5卷积层
        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=1)
        #线路4,3x3最大池化层后接1x1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3,stride=1,padding=1)
        self.p4_2 = nn.Conv2d(in_c,c4,kernrl_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)#在通道维上连结输出
                    
  1. GoogLeNet和VGG一样,在主体卷积部分中使用5个模块(block)。每个模块之间使用步幅为2的3x3最大池化层来减小输出高宽。第一模块使用一个64通道的7x7卷积层。第一个模块对应Inception中的第一条线路。
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))
  1. 第二个模块使用2个卷积层,首先是64通道的1x1卷积层,然后是将通道增大3倍的3x3卷积层。第二个模块对应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))
  1. 第三模块串联2个完整的Inception块,第一个Inception块的输出通道数为64+128+32+32=256,其中4条线路的输出通道数比例为64:128:32:32=2;4:1:1,其中第2第3条线路先分别将输入通道数减小至96/192=1/2,16/192=1/12,然后再接上第2层卷积层。
  2. 第2个Inception模块输出通道数增加至128+192+96+64=480,每条线路的输出通道数之比为128:192:96:64=4:6:3:2,其中第2第3条线路先分别将输入通道数减小到128/256=1/2,32/256=1/8。
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))
  1. 第4模块更加复杂,它串联了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.这些线路的通道数分配和第三模块中的类似。首先含3x3卷积层的第二条线路输出最多通道,其次是仅含1x1卷积层的第一条线路输出第二多的通道数目,之后是含5x5卷积层的第三条线路和含3x3最大池化层的第四条线路输出第三多的通道数目。其中第二、第三条线路都会按比例减小通道数,这些比例在各个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))
  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),
                   d2l.GlobalAvgPool2d())
net = nn.Sequential(b1,b2,b3,b4,b5,
                    d2l.FlattenLayer(),
                    nn.Linear(1024,10))
  1. GoogLeNet的计算复杂而且不如VGG那样便于修改通道数,所以下面将输入的高和宽从224降低到96来简化计算。下面的代码实现展示各个模块之间的输出的形状变化。
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.c
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值