李沐动手学深度学习V2 - CNN全部基础知识

1.卷积层

1.1 从全连接层到卷积层

多层感知机十分适合处理表格数据,其中行对应样本,列对应特征。 对于表格数据,我们寻找的模式可能涉及特征之间的交互,但是我们不能预先假设任何与特征交互相关的先验结构。 此时,多层感知机可能是最好的选择,然而对于高维感知数据,这种缺少结构的网络可能会变得不实用。
例如,在之前猫狗分类的例子中:假设我们有一个足够充分的照片数据集,数据集中是拥有标注的照片,每张照片具有百万级像素,这意味着网络的每次输入都有一百万个维度。 即使将隐藏层维度降低到1000,这个全连接层也将有 106×103=109 个参数。 想要训练这个模型将不可实现,因为需要有大量的GPU、分布式优化训练的经验和超乎常人的耐心。
有些人要求百万像素的分辨率可能不是必要的。 然而,即使分辨率减小为十万像素,使用1000个隐藏单元的隐藏层也可能不足以学习到良好的图像特征,在真实的系统中我们仍然需要数十亿个参数。 此外,拟合如此多的参数还需要收集大量的数据。 然而,如今人类和机器都能很好地区分猫和狗:这是因为图像中本就拥有丰富的结构,而这些结构可以被人类和机器学习模型使用。 卷积神经网络(convolutional neural networks,CNN)是机器学习利用自然图像中一些已知结构的创造性方法。

1.2 CNN特性

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

1.3 手动实现卷积计算(互相关计算)

在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。 当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。

#代码实现
def conv2d(X,K):
    h,w = K.shape
    Y_h = X.shape[0]-h+1
    Y_w = X.shape[1]-w+1
    Y = torch.zeros((Y_h,Y_w))
    for i in range(Y_h):
        for j in range(Y_w):
            Y[i,j] = (X[i:i+h,j:j+w]*K).sum() #计算Y[i][j]的值
    return Y
X = torch.tensor([[0,1,2],[3,4,5],[6,7,8]])
K = torch.tensor([[1,2],[2,6]])
print(conv2d(X,K))
#输出结果:
#tensor([[32., 43.],
#        [65., 76.]])

1.4 卷积层实现

卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。在训练基于卷积层的模型时,我们也随机初始化卷积核权重。
基于上面定义的conv2d()函数实现二维卷积层。在__init__构造函数中,将weight和bias声明为两个模型参数。前向传播函数调用conv2d()函数并添加偏置。
高度和宽度分别为 ℎ 和 𝑤 的卷积核可以被称为 ℎ×𝑤 卷积或 ℎ×𝑤 卷积核。 我们也将带有 ℎ×𝑤 卷积核的卷积层称为 ℎ×𝑤 卷积层(包含输入层,卷积核,输出层)。

#定义一个二维卷积层网络
class Conv2d(nn.Module):
    def __init__(self,K):
        super(Conv2d, self).__init__()
        self.weight = nn.parameter(torch.randn(K.shape)) #初始化kernel权重
        self.bias = nn.parameter(torch.zeros(1)) #初始化bias
    def forward(self,X):
        return conv2d(X,self.weight)+self.bias #进行一次卷积操作

1.5 图像中目标的边缘检测简单例子

  1. 通过找到像素变化的位置,来(检测图像中不同颜色的边缘)。 首先,我们构造一个 6×8 像素的黑白图像。中间四列为黑色( 0 ),其余像素为白色( 1 )。
  2. 接着构造一个高度为 1 、宽度为 2 的卷积核K。当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。
  3. 对参数X(输入)和K(卷积核)执行互相关运算。 输出Y中1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为 0 。
  4. 将输入的二维图像转置,再进行上面的互相关运算。 其输出表示之前检测到的垂直边缘消失了。 表明这个卷积核K只可以检测垂直边缘,无法检测水平边缘。
#代码实现
X = torch.ones((6,8))
X[:,2:6] = 0
print(X)
K = torch.tensor([[1.0,-1.0]]) #检测1,0边界信息
Y = conv2d(X,K)
print(Y) #输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘
print(conv2d(X.t(),K)) #将矩阵转值,再放入卷积核里面进行计算,结果卷积核K只可以检测垂直边缘,无法检测水平边缘
'''
输出结果如下:
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.]])
tensor([[ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.]])
tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]])
'''

1.6 卷积核参数学习

通过仅查看“输入-输出”对来学习由X生成Y的卷积核。 先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核。

# 使用Pytorch内置的卷积层构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
net = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(1,2),bias=False) #忽略偏置
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = torch.reshape(X,(1,1,6,8)) #将输入变为bitch_size = 1,chanels = 1,h = 6,w = 8
Y = torch.reshape(Y,(1,1,6,7))#将输出变为bitch_size = 1,chanels = 1,h = 6,w = 7
lr = 3e-2
for i in range(10):
    Y_hat = net(X)
    loss = (Y_hat - Y)**2
    net.zero_grad()
    loss.sum().backward()
    net.weight.data -= lr*net.weight.grad #减去梯度,更新权重参数
    if((i+1)%2 == 0):
        print(f'训练第{i+1}轮后的误差为:{loss.sum()}')
print(net.weight.data.reshape((1,2))) #查看学到的卷积核权重,权重与前面定义的卷积核权重[1.0,-1.0]很接近
#print(net.bias.data) #当卷积层进行卷积操作时,bias只有一维,表示对进行卷积操作后再进行偏移
'''
输出结果:
训练第2轮后的误差为:11.319974899291992
训练第4轮后的误差为:2.3796136379241943
训练第6轮后的误差为:0.5960201025009155
训练第8轮后的误差为:0.18059949576854706
训练第10轮后的误差为:0.06331478804349899
tensor([[ 1.0085, -0.9597]]) 
'''

1.7 特征映射和感受野:

卷积层的输出也被称为特征映射(feature map),因为它可以被视为一个输入映射到下一层的空间维度的转换器。
在卷积神经网络中,对于某一层的任意元素 𝑥 ,其感受野(receptive field)是指在前向传播期间可能影响 𝑥 计算的所有元素(来自所有先前层)。
注意,感受野可能大于输入的实际大小。比如如下图所示: 给定 2×2 卷积核,阴影输出元素值 19 的感受野是输入阴影部分的四个元素。 假设之前输出为 𝐘 ,其大小为 2×2 ,现在我们在其后附加一个卷积层,该卷积层以 𝐘 为输入,输出单个元素 𝑧 。 在这种情况下, 𝐘 上的 𝑧 的感受野包括 𝐘 的所有四个元素,而输入的感受野包括最初所有九个输入元素。 因此,当一个特征图中的任意元素需要检测更广区域的输入特征时,我们可以构建一个更深的网络。

1.8 小结

1)二维卷积层的核心计算是二维互相关运算(也可认为是卷积计算)。最简单的形式是,对二维输入数据和卷积核执行互相关操作,然后添加一个偏置。
2)我们可以设计一个卷积核来检测图像的边缘。
3)我们可以从数据中学习卷积核的参数。
4)学习卷积核时,无论用严格卷积运算或互相关运算,卷积层的输出不会受太大影响。(卷积运算和互相关运算在数学上面定义是不同的,在卷积神经网络中不区分这两者概念,都当成相同的)
5)当需要检测输入特征中更广区域时,我们可以构建一个更深的卷积网络。
6)图像的平移不变性使我们以相同的方式处理局部图像,而不在乎它的位置。
7) 在图像处理中,卷积层通常比全连接层需要更少的参数,但依旧获得高效用的模型
8)多个输入和输出通道使模型在每个空间位置可以获取图像的多方面特征,比如:纹理、边缘、锐化等特征
9)局部性意味着计算相应的隐藏表示只需一小部分局部图像像素。
10)卷积神经网络(CNN)是一类特殊的神经网络,它可以包含多个卷积层。

1.9 本节全部代码:

import torch
from torch import nn


def conv2d(X,K):
    h,w = K.shape
    Y_h = X.shape[0]-h+1
    Y_w = X.shape[1]-w+1
    Y = torch.zeros((Y_h,Y_w))
    for i in range(Y_h):
        for j in range(Y_w):
            Y[i,j] = (X[i:i+h,j:j+w]*K).sum() #计算Y[i][j]的值
    return Y
X = torch.tensor([[0,1,2],[3,4,5],[6,7,8]])
K = torch.tensor([[1,2],[2,6]])
print(conv2d(X,K))
#定义一个二维卷积层网络
class Conv2d(nn.Module):
    def __init__(self,K):
        super(Conv2d, self).__init__()
        self.weight = nn.parameter(torch.randn(K.shape)) #初始化kernel权重
        self.bias = nn.parameter(torch.zeros(1)) #初始化bias
    def forward(self,X):
        return conv2d(X,self.weight)+self.bias #进行一次卷积操作

X = torch.ones((6,8))
X[:,2:6] = 0
print(X)
K = torch.tensor([[1.0,-1.0]]) #检测1,0边界信息
Y = conv2d(X,K)
print(Y) #输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘
print(conv2d(X.t(),K)) #将矩阵转值,再放入卷积核里面进行计算,结果卷积核K只可以检测垂直边缘,无法检测水平边缘
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
net = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(1,2),bias=False) #忽略偏移值
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = torch.reshape(X,(1,1,6,8)) #将输入变为bitch_size = 1,chanels = 1,h = 6,w = 8
Y = torch.reshape(Y,(1,1,6,7))#将输出变为bitch_size = 1,chanels = 1,h = 6,w = 7
lr = 3e-2
for i in range(10):
    Y_hat = net(X)
    loss = (Y_hat - Y)**2
    net.zero_grad()
    loss.sum().backward()
    net.weight.data -= lr*net.weight.grad #减去梯度,更新权重参数
    if((i+1)%2 == 0):
        print(f'训练第{i+1}轮后的误差为:{loss.sum()}')
print(net.weight.data.reshape((1,2)))
#print(net.bias.data) #当卷积层进行卷积操作时,bias只有一维,表示对进行卷积操作后再进行偏移

2.填充和步幅

假设以下情景: 有时,在应用了连续的卷积之后,我们最终得到的输出远小于输入大小。这是由于卷积核的宽度和高度通常大于 1 所导致的。比如,一个 240×240 像素的图像,经过 10 层 5×5 的卷积后,将减少到 200×200 像素。如此一来,原始图像的边界丢失了许多有用信息。而填充是解决此问题最有效的方法。 有时,我们可能希望大幅降低图像的宽度和高度。例如,如果我们发现原始的输入分辨率十分冗余。步幅则可以在这类情况下提供帮助。

2.1 填充Padding

  1. 在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。 解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是 0 )
  2. 通常,如果我们添加 𝑝_ℎ 行填充(大约一半在顶部,一半在底部)和 𝑝_𝑤 列填充(左侧大约一半,右侧一半),则输出形状将为(𝑛_ℎ−𝑘_ℎ+𝑝_ℎ+1)×(𝑛_𝑤−𝑘_𝑤+𝑝_𝑤+1)。表明输出的高度和宽度将比没有Paddings时分别增加 𝑝_ℎ 和 𝑝_𝑤
  3. 在许多情况下,我们需要设置 𝑝_ℎ=𝑘_ℎ−1 和 𝑝_𝑤=𝑘_𝑤−1 ,使输入和输出具有相同的高度和宽度。 这样可以在构建网络时更容易地预测每个图层的输出形状。假设 𝑘_ℎ 是奇数,我们将在高度的两侧填充 𝑝_ℎ/2 行。 如果 𝑘_ℎ 是偶数,则一种可能性是在输入顶部填充 ⌈𝑝_ℎ/2⌉ 行,在底部填充 ⌊𝑝_ℎ/2⌋ 行。同理,我们填充宽度的两侧。
  4. 卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。 选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
  5. 使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X,当满足:
    a. 卷积核的大小是奇数;
    b. 所有边的填充行数和列数相同;
    c. 输出与输入具有相同高度和宽度 则可以得出:输出Y[i, j]是通过以输入X[i, j]为中心,与卷积核进行互相关计算得到的。
#创建一个高度和宽度为3的二维卷积层,并(在所有侧边填充1个像素)。给定高度和宽度为8的输入,则输出的高度和宽度也是8.
#添加padding
def comp_conv2d(Con2d,X):
	#(1,1)表示批量大小和通道数都是1
    X = X.reshape((1,1)+X.shape)
    Y = Con2d(X)
    #省略前两个维度:批量大小和通道
    return Y.reshape(Y.shape[2:])
#每边都填充了1行或1列,因此总共添加了2行或2列
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=3,padding=1)
X = torch.randn(size=(8,8))
Z = comp_conv2d(Con2d,X)
print(Z.shape)
#当卷积核的高度和宽度不同时,我们可以[填充不同的高度和宽度],使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。
K = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(5,3),padding=(2,1))
print(comp_conv2d(K,X).shape)
#输出:
#torch.Size([8, 8])
#torch.Size([8, 8])

2.2 步幅Stride : 卷积核每次滑动元素的数量

  1. 在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 在上面的例子中,我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。

  2. 卷积层输出形状计算公式:
    通常,当垂直步幅为 𝑠_ℎ 、水平步幅为 𝑠_𝑤 时,输出形状为
    ⌊(𝑛_ℎ−𝑘_ℎ+𝑝_ℎ+𝑠_ℎ)/𝑠_ℎ⌋×⌊(𝑛_𝑤−𝑘_𝑤+𝑝_𝑤+𝑠_𝑤)/𝑠_𝑤⌋.
    如果我们设置了 𝑝_ℎ=𝑘_ℎ−1 和 𝑝_𝑤=𝑘_𝑤−1 ,则输出形状将简化为 ⌊(𝑛_ℎ+𝑠_ℎ−1)/𝑠_ℎ⌋×⌊(𝑛_𝑤+𝑠_𝑤−1)/𝑠_𝑤⌋ ,结果向下取整。 更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 (𝑛_ℎ/𝑠_ℎ)×(𝑛_𝑤/𝑠_𝑤) 。如下所示

    卷积层输出形状计算公式

  3. Stride代码如下:

#添加stride
#将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=3,padding=1,stride=2)
print(comp_conv2d(Con2d,X).shape)#添加stride后,输出维度是输入的一半
#padding和stride的宽高不一样时
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(3,5),padding=(0,1),stride=(3,4))
print(comp_conv2d(Con2d,X).shape)
  1. 为了简洁起见,当输入高度和宽度两侧的填充数量分别为 𝑝_ℎ 和 𝑝_𝑤 时,我们称之为填充 (𝑝_ℎ,𝑝_𝑤) 。当 𝑝_ℎ=𝑝_𝑤=𝑝 时,填充是 𝑝 。同理,当高度和宽度上的步幅分别为 𝑠_ℎ 和 𝑠_𝑤 时,我们称之为步幅 (𝑠_ℎ,𝑠_𝑤) 。当时的步幅为 𝑠_ℎ=𝑠_𝑤=𝑠 时,步幅为 𝑠 。默认情况下,填充为0,步幅为1。在实践中,我们很少使用不一致的步幅或填充,也就是说,我们通常有 𝑝_ℎ=𝑝_𝑤=p 和 𝑠_ℎ=𝑠_𝑤=s。

2.3 小结

1)填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
2)步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 1/𝑛 ( 𝑛 是一个大于 1 的整数),利于减少输入冗余维度。
3)填充和步幅可用于有效地调整数据的维度。

2.4 本节全部代码:

#添加padding
def comp_conv2d(Con2d,X):
    X = X.reshape((1,1)+X.shape)
    Y = Con2d(X)
    return Y.reshape(Y.shape[2:])
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=3,padding=1)
X = torch.randn(size=(8,8))
Z = comp_conv2d(Con2d,X)
print(Z.shape)
K = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(5,3),padding=(2,1))
print(comp_conv2d(K,X).shape)
#添加stride
##将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=3,padding=1,stride=2)
print(comp_conv2d(Con2d,X).shape)#添加stride后,输出维度是输入的一半
#padding和stride不一样时
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(3,5),padding=(0,1),stride=(3,4))
print(comp_conv2d(Con2d,X).shape)

3 多输入通道和多输出通道

3.1 多输入通道

**当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。**假设输入的通道数为 𝑐_i ,那么卷积核的输入通道数也需要为 𝑐_i 。如果卷积核的窗口形状是 𝑘_ℎ×𝑘_𝑤 ,那么当 𝑐_i=1 时,我们可以把卷积核看作形状为 𝑘_ℎ×𝑘_𝑤 的二维张量。
当 𝑐_𝑖>1 时,我们卷积核的每个输入通道将包含形状为 𝑘_ℎ×𝑘_𝑤 的张量。将这些张量 𝑐_𝑖 连结在一起可以得到形状为 𝑐_𝑖×𝑘_ℎ×𝑘_𝑤 的卷积核。由于输入和卷积核都有 𝑐_𝑖 个通道,我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将 𝑐_𝑖 的结果相加)得到一个二维输出张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。
多输入通道和多通道卷积核计算示例:
一个具有两个输入通道的二维互相关运算,阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素: (1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56 。
含2个输入通道的卷积核计算

3.2 代码实现一下多输入通道互相关运算(总之就是对每个通道执行互相关(卷积)操作,然后将结果相加)

#多输入通道
def conv2d_multichanels_input(X,K):
    return sum(conv2d(x,k) for x,k in zip(X,K)) #zip()相当于把X,K第一个通道压缩(组合)在一起,把X,K第二个通道压缩(组合)在一起,因此经过for循环x,k为X,K第一个通道,经过conv2d(x,k)函数互操作后,然后接着是X,K第二个通道,进行同样的操作
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
print(conv2d_multichanels_input(X,K))
#输出结果:
#tensor([[ 56.,  72.],
#        [104., 120.]])

3.3 多输出通道:

在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更多的通道数目。直观地说,我们可以将每个通道看作是对不同特征(比如不同纹理,边缘等特征)的响应。而现实可能更为复杂一些,(因为每个通道不是独立学习的),而是为了共同使用而优化的(可能是同时学习多个通道特征,组合多个通道特征)。(因此,多输出通道并不仅是学习多个单通道的检测器。)
用 𝑐_𝑖 和 𝑐_𝑜 分别表示输入和输出通道的数目,并让 𝑘_ℎ 和 𝑘_𝑤 为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为 𝑐_𝑖×𝑘_ℎ×𝑘_𝑤 的卷积核张量,这样卷积核的形状是 𝑐_𝑜×𝑐_𝑖×𝑘_ℎ×𝑘_𝑤 。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。

3.4 代码手动实现一个多个输出通道的互相关计算函数:

#多输入多输出通道
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
def conv2d_multichanels_input_output(X,K):
    #for k in K 表示每次从K的第一维拿出数据,相当于K[i],得到的数据是一个三维tensor,把拿出的数据与输入X做互相关操作(卷积操作),得到第一个输出通道二维张量,每次遍历执行相同操作,遍历完后就得到多输出通道的结果
    return torch.stack([conv2d_multichanels_input(X,k) for k in K],dim=0)
#通过将核张量K与K+1(K中每个元素加 1 )和K+2(广播机制,K中每个元素加2)连接起来,构造了一个具有2个输入通道,3个输出通道,卷积核尺寸为(output_chanels=3,input_chanels=2,卷积核高h=2,卷积核宽w=2)的卷积层。
K = torch.stack((K,K+1,K+2),dim=0) #dim = 0表示给元组[k,k+1,k+2]数据多增加一维,因此最后得到的结果是将多个二维张量组合成一个三维张量,增加了一个维度
print(conv2d_multichanels_input_output(X,K))
'''
输出结果:
tensor([[[ 56.,  72.],
         [104., 120.]],

        [[ 76., 100.],
         [148., 172.]],

        [[ 96., 128.],
         [192., 224.]]])
'''

3.5 1x1卷积层

1×1 卷积,即𝑘_ℎ=𝑘_𝑤=1,看起来似乎没有多大意义。 毕竟,卷积的本质是有效提取相邻像素间的相关特征,而1×1卷积显然没有此作用。 尽管如此,1×1仍然十分流行,经常包含在复杂深层网络的设计中。
因为使用了最小窗口, 1×1 卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力,但把不同通道相同位置的像素以全连接的方式线性组合在一起,也就是把不同通道相同位置的像素(跨通道)融合在一起。 其实 1×1 卷积的唯一计算发生在通道上。
下图展示了使用 1×1 卷积核与 3 个输入通道和 2 个输出通道的互相关计算。 这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。 我们可以将 1×1 卷积层看作是在每个像素位置应用的全连接层,以 𝑐_𝑖 个输入值转换为 𝑐_𝑜 个输出值。 因为这仍然是一个卷积层,所以跨像素的权重是一致的。 同时, 1×1 卷积层需要的权重维度为 𝑐_𝑜×𝑐_𝑖 ,再额外加上一个偏置。
1x1 卷积层

3.6 代码:使用全连接层实现 1×1 卷积层

# 1*1卷积层
def conv2d_multichanels_input_output_1x1(X,K):
    x_in,h,w = X.shape
    k_out = K.shape[0]
    X = X.reshape((x_in,h*w)) #对输入X数据形状进行调整
    K = K.reshape((k_out,x_in)) #对卷积核数据形状进行调整
    Y = torch.matmul(K,X) # 全连接层中的矩阵乘法
    return Y.reshape((k_out,h,w)) #对输出数据形状进行调整

X = torch.normal(0,1,(3,3,2))
K = torch.normal(0,1,(2,3,1,1))
Y_1x1 = conv2d_multichanels_input_output_1x1(X,K) #使用全连接层方式计算1*1卷积核
Y = conv2d_multichanels_input_output(X,K) #使用卷积方式计算1*1卷积核
print((abs(Y_1x1-Y)).sum() <1e-6) #判断使用卷积方式和全连接层方式计算结果是否相同
'''
输出结果:
tensor(True)
'''

3.7 小结

  1. 多输入多输出通道可以用来扩展卷积层的模型。
  2. 当以每像素为基础应用时, 1×1 卷积层相当于全连接层。
  3. 1×1 卷积层通常用于调整网络层的输出通道数量和控制模型复杂性(减少卷积核权重参数)。

3.8 本节全部代码:

#多输入通道
def conv2d_multichanels_input(X,K):
    return sum(conv2d(x,k) for x,k in zip(X,K)) #zip()相当于把X,K第一个通道压缩(组合)在一起,把X,K第二个通道压缩(组合)在一起,因此经过for循环x,k为X,K第一个通道,经过conv2d(x,k)函数互操作后,然后接着是X,K第二个通道,进行同样的操作
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
print(conv2d_multichanels_input(X,K))

#多输入多输出通道
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
def conv2d_multichanels_input_output(X,K):
    #for k in K 表示每次从K的第一维拿出数据,相当于K[i],得到的数据是一个三维tensor,把拿出的数据与输入X做互相关操作(卷积操作),得到第一个输出通道二维张量,每次遍历执行相同操作,遍历完后就得到多输出通道的结果
    return torch.stack([conv2d_multichanels_input(X,k) for k in K],dim=0)
#通过将核张量K与K+1(K中每个元素加 1 )和K+2(广播机制,K中每个元素加2)连接起来,构造了一个具有2个输入通道,3个输出通道,卷积核尺寸为(output_chanels=3,input_chanels=2,卷积核高h=2,卷积核宽w=2)的卷积层。
K = torch.stack((K,K+1,K+2),dim=0)#dim = 0表示给元组[k,k+1,k+2]数据多增加一维,因此最后得到的结果是将多个二维张量组合成一个三维张量,增加了一个维度
print(conv2d_multichanels_input_output(X,K))

# 1*1卷积层
def conv2d_multichanels_input_output_1x1(X,K):
    x_in,h,w = X.shape
    k_out = K.shape[0]
    X = X.reshape((x_in,h*w)) #对输入X数据形状进行调整
    K = K.reshape((k_out,x_in)) #对卷积核数据形状进行调整
    Y = torch.matmul(K,X) # 全连接层中的矩阵乘法
    return Y.reshape((k_out,h,w)) #对输出数据形状进行调整

X = torch.normal(0,1,(3,3,2))
K = torch.normal(0,1,(2,3,1,1))
Y_1x1 = conv2d_multichanels_input_output_1x1(X,K) #使用全连接层方式计算1*1卷积核
Y = conv2d_multichanels_input_output(X,K) #使用卷积方式计算1*1卷积核
print((abs(Y_1x1-Y)).sum() <1e-6) #判断使用卷积方式和全连接层方式计算结果是否相同

4 池化层(汇聚层)(Pooling层)

通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受(输入)就越大。
而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”),所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
当检测较底层的特征时(如边缘等特征),我们通常希望这些特征保持某种程度上的平移不变性。例如,如果我们拍摄黑白之间轮廓清晰的图像X,并将整个图像向左移动一个像素,即Z[i, j] = X[i, j + 1],则新图像Z的输出可能大不相同。而在现实中,随着拍摄角度的移动,任何物体几乎不可能发生在同一像素上。即使用三脚架拍摄一个静止的物体,由于快门的移动而引起的相机振动,可能会使所有物体左右移动一个像素(除了高端相机配备了特殊功能来解决这个问题)。
汇聚(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。

4.1 最大汇聚层和平均汇聚层

与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。 然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。 相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。
在这两种情况下,与互相关运算符一样,汇聚窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层,如下图所示最大汇聚层。
最大汇聚层
汇聚窗口形状为 𝑝×𝑞 的汇聚层称为 𝑝×𝑞 汇聚层,汇聚操作称为 𝑝×𝑞 汇聚。
对于前面开头提到的对边缘检测示例,现在我们将使用卷积层的输出作为 2×2 最大汇聚的输入。 设置卷积层输入为X,汇聚层输出为Y。 无论X[i, j]和X[i, j + 1]的值相同与否,或X[i, j + 1]和X[i, j + 2]的值相同与否,汇聚层始终输出Y[i, j] = 1。 也就是说,使用 2×2 最大汇聚层,即使在高度或宽度上移动一个元素,卷积层仍然可以识别到模式

4.2 代码实现汇聚层的前向传播,输出为输入中每个区域的最大值或平均值。

#池化层
def pool2d(X,pool_size,mode='max'):
    p_h,p_w = pool_size #池化层高和宽
    Y = torch.zeros(size=(X.shape[0]-p_h+1,X.shape[1]-p_w+1))#初始化输出
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            if(mode == 'max'):
                Y[i,j] = X[i:i+p_h,j:j+p_w].max() #计算第i行到i+p_h行以及第j列到第j+p_w列所组成的矩阵的最大值作为输出Y[i, j]的值
            if(mode == 'avg'):
                Y[i, j] = X[i:i + p_h, j:j + p_w].mean() #计算第i行到i+p_h行以及第j列到第j+p_w列所组成的矩阵的平均值作为输出Y[i, j]的值
    return Y
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
print(pool2d(X, (2, 2))) #使用最大池化层
print(pool2d(X,(2,2),'avg')) #使用平均池化层
'''
输出结果:
tensor([[4., 5.],
        [7., 8.]])
tensor([[2., 3.],
        [5., 6.]])
'''

4.3 池化层填充和步幅

与卷积层一样,汇聚层也可以改变输出形状。和之前一样,我们可以通过填充和步幅以获得所需的输出形状。
输出形状计算公式:注意此处 k_h,h_w为汇聚层的高和宽
通常,当垂直步幅为 𝑠_ℎ 、水平步幅为 𝑠_𝑤 时,输出形状为⌊(𝑛_ℎ−𝑘_ℎ+𝑝_ℎ+𝑠_ℎ)/𝑠_ℎ⌋×⌊(𝑛_𝑤−𝑘_𝑤+𝑝_𝑤+𝑠_𝑤)/𝑠_𝑤⌋.
如果我们设置了 𝑝_ℎ=𝑘_ℎ−1 和 𝑝_𝑤=𝑘_𝑤−1 ,则输出形状将简化为 ⌊(𝑛_ℎ+𝑠_ℎ−1)/𝑠_ℎ⌋×⌊(𝑛_𝑤+𝑠_𝑤−1)/𝑠_𝑤⌋ ,结果向下取整。 更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 (𝑛_ℎ/𝑠_ℎ)×(𝑛_𝑤/𝑠_𝑤) 。如下所示

汇聚层输出形状计算公式
默认情况下,(深度学习框架中的步幅与汇聚窗口的大小相同)。 因此,如果我们使用形状为(3, 3)的汇聚窗口,那么默认情况下,我们得到的步幅形状为(3, 3)。

4.4 Pytorch中使用池化层填充和步幅的代码如下:

#池化层填充和步幅
X = torch.arange(16,dtype=torch.float32).reshape((1,1,4,4)) #输入是四维张量,(batch_size,chanels,h,w) 表示构造了一个输入张量X,它有四个维度,其中样本数和通道数都是1。
pool2d_max = nn.MaxPool2d(3) #深度学习框架中的步幅与汇聚窗口的大小相同,此处都为3
Y = pool2d_max(X) #输出也是四维张量,(batch_size,chanels,h,w)
print(Y)
pool2d_max1 = nn.MaxPool2d(kernel_size=3,padding=1,stride=2) #填充和步幅可以手动设定
print(pool2d_max1(X))
pool2d_max2 = nn.MaxPool2d(kernel_size=(2,3),stride=(2,3),padding=(0,1)) #设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度
print(pool2d_max2(X))
'''
输出结果:
tensor([[[[10.]]]])
tensor([[[[ 5.,  7.],
          [13., 15.]]]])
tensor([[[[ 5.,  7.],
          [13., 15.]]]])
'''

4.5 汇聚层多个输入输出通道

在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总(卷积核窗口对多个输入通道分别内积求和得出的值再相加)。 这意味着汇聚层的输出通道数与输入通道数相同。
代码如下:

#池化层多输入通道
X = torch.cat((X,X+1),dim=1) #在通道维度上连结张量X和X + 1(广播机制,在X中所有元素都加1),以构建具有2个通道的输入,dim=1表示通道维度上,0表示batch_size维度上
Y = pool2d_max1(X)
print(Y)#输出通道的数量跟输入通道的数量相同,也是2
'''
输出结果:
tensor([[[[ 5.,  7.],
          [13., 15.]],

         [[ 6.,  8.],
          [14., 16.]]]])
'''

4.6 小结:

  1. 对于给定输入元素,最大汇聚层会输出该窗口内的最大值,平均汇聚层会输出该窗口内的平均值。
  2. 汇聚层的主要优点之一是减轻卷积层对位置的过度敏感。
  3. 我们可以手动指定汇聚层的填充和步幅。
  4. 使用最大汇聚层以及大于1的步幅,可减少空间维度(如高度和宽度)(输出维度比输入维度小)。
  5. 汇聚层的输出通道数与输入通道数相同。

4.7 本节全部代码:

#池化层
def pool2d(X,pool_size,mode='max'):
    p_h,p_w = pool_size #池化层高和宽
    Y = torch.zeros(size=(X.shape[0]-p_h+1,X.shape[1]-p_w+1))#初始化输出
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            if(mode == 'max'):
                Y[i,j] = X[i:i+p_h,j:j+p_w].max() #计算第i行到i+p_h行以及第j列到第j+p_w列所组成的矩阵的最大值作为输出Y[i, j]的值
            if(mode == 'avg'):
                Y[i, j] = X[i:i + p_h, j:j + p_w].mean() #计算第i行到i+p_h行以及第j列到第j+p_w列所组成的矩阵的平均值作为输出Y[i, j]的值
    return Y
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
print(pool2d(X, (2, 2))) #使用最大池化层
print(pool2d(X,(2,2),'avg')) #使用平均池化层

#池化层填充和步幅
X = torch.arange(16,dtype=torch.float32).reshape((1,1,4,4)) #输入是四维张量,(batch_size,chanels,h,w)
pool2d_max = nn.MaxPool2d(3) #深度学习框架中的步幅与汇聚窗口的大小相同,此处都为3
Y = pool2d_max(X) #输出也是四维张量,(batch_size,chanels,h,w)
print(Y)
pool2d_max1 = nn.MaxPool2d(kernel_size=3,padding=1,stride=2) #填充和步幅可以手动设定
print(pool2d_max1(X))
pool2d_max2 = nn.MaxPool2d(kernel_size=(2,3),stride=(2,3),padding=(0,1)) #设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度
print(pool2d_max2(X))
#池化层多输入通道
X = torch.cat((X,X+1),dim=1) #在通道维度上连结张量X和X + 1(广播机制,在X中所有元素都加1),以构建具有2个通道的输入,dim=1表示通道维度上,0表示batch_size维度上
Y = pool2d_max1(X)
print(Y)#输出通道的数量跟输入通道的数量相同,也是2

5. 本章全部代码:

import torch
from torch import nn

#卷积层
def conv2d(X,K):
    h,w = K.shape
    Y_h = X.shape[0]-h+1
    Y_w = X.shape[1]-w+1
    Y = torch.zeros((Y_h,Y_w))
    for i in range(Y_h):
        for j in range(Y_w):
            Y[i,j] = (X[i:i+h,j:j+w]*K).sum() #计算Y[i][j]的值
    return Y
X = torch.tensor([[0,1,2],[3,4,5],[6,7,8]])
K = torch.tensor([[1,2],[2,6]])
print(conv2d(X,K))
#定义一个二维卷积层网络
class Conv2d(nn.Module):
    def __init__(self,K):
        super(Conv2d, self).__init__()
        self.weight = nn.parameter(torch.randn(K.shape)) #初始化kernel权重
        self.bias = nn.parameter(torch.zeros(1)) #初始化bias
    def forward(self,X):
        return conv2d(X,self.weight)+self.bias #进行一次卷积操作

X = torch.ones((6,8))
X[:,2:6] = 0
print(X)
K = torch.tensor([[1.0,-1.0]]) #检测1,0边界信息
Y = conv2d(X,K)
print(Y) #输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘
print(conv2d(X.t(),K)) #将矩阵转值,再放入卷积核里面进行计算,结果卷积核K只可以检测垂直边缘,无法检测水平边缘
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
net = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(1,2),bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = torch.reshape(X,(1,1,6,8)) #将输入变为bitch_size = 1,chanels = 1,h = 6,w = 8
Y = torch.reshape(Y,(1,1,6,7))#将输出变为bitch_size = 1,chanels = 1,h = 6,w = 7
lr = 3e-2
for i in range(10):
    Y_hat = net(X)
    loss = (Y_hat - Y)**2
    net.zero_grad()
    loss.sum().backward()
    net.weight.data -= lr*net.weight.grad #减去梯度,更新权重参数
    if((i+1)%2 == 0):
        print(f'训练第{i+1}轮后的误差为:{loss.sum()}')
print(net.weight.data.reshape((1,2)))
#print(net.bias.data) #当卷积层进行卷积操作时,bias只有一维,表示对进行卷积操作后再进行偏移


#添加padding
def comp_conv2d(Con2d,X):
    X = X.reshape((1,1)+X.shape)
    Y = Con2d(X)
    return Y.reshape(Y.shape[2:])
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=3,padding=1)
X = torch.randn(size=(8,8))
Z = comp_conv2d(Con2d,X)
print(Z.shape)
K = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(5,3),padding=(2,1))
print(comp_conv2d(K,X).shape)

#添加stride
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=3,padding=1,stride=2)
print(comp_conv2d(Con2d,X).shape)#添加stride后,输出维度是输入的一半
#padding和stride不一样时
Con2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(3,5),padding=(0,1),stride=(3,4))
print(comp_conv2d(Con2d,X).shape)

#多输入通道
def conv2d_multichanels_input(X,K):
    return sum(conv2d(x,k) for x,k in zip(X,K)) #zip()相当于把X,K第一个通道压缩(组合)在一起,把X,K第二个通道压缩(组合)在一起,因此经过for循环x,k为X,K第一个通道,经过conv2d(x,k)函数互操作后,然后接着是X,K第二个通道,进行同样的操作
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
print(conv2d_multichanels_input(X,K))

#多输入多输出通道
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
def conv2d_multichanels_input_output(X,K):
    #for k in K 表示每次从K的第一维拿出数据,相当于K[i],得到的数据是一个三维tensor,把拿出的数据与输入X做互相关操作(卷积操作),得到第一个输出通道二维张量,每次遍历执行相同操作,遍历完后就得到多输出通道的结果
    return torch.stack([conv2d_multichanels_input(X,k) for k in K],dim=0)
#通过将核张量K与K+1(K中每个元素加 1 )和K+2(广播机制,K中每个元素加2)连接起来,构造了一个具有2个输入通道,3个输出通道,卷积核尺寸为(output_chanels=3,input_chanels=2,卷积核高h=2,卷积核宽w=2)的卷积层。
K = torch.stack((K,K+1,K+2),dim=0)#dim = 0表示给元组[k,k+1,k+2]数据多增加一维,因此最后得到的结果是将多个二维张量组合成一个三维张量,增加了一个维度
print(conv2d_multichanels_input_output(X,K))

# 1*1卷积层
def conv2d_multichanels_input_output_1x1(X,K):
    x_in,h,w = X.shape
    k_out = K.shape[0]
    X = X.reshape((x_in,h*w)) #对输入X数据形状进行调整
    K = K.reshape((k_out,x_in)) #对卷积核数据形状进行调整
    Y = torch.matmul(K,X) # 全连接层中的矩阵乘法
    return Y.reshape((k_out,h,w)) #对输出数据形状进行调整

X = torch.normal(0,1,(3,3,2))
K = torch.normal(0,1,(2,3,1,1))
Y_1x1 = conv2d_multichanels_input_output_1x1(X,K) #使用全连接层方式计算1*1卷积核
Y = conv2d_multichanels_input_output(X,K) #使用卷积方式计算1*1卷积核
print((abs(Y_1x1-Y)).sum() <1e-6) #判断使用卷积方式和全连接层方式计算结果是否相同

#池化层
def pool2d(X,pool_size,mode='max'):
    p_h,p_w = pool_size #池化层高和宽
    Y = torch.zeros(size=(X.shape[0]-p_h+1,X.shape[1]-p_w+1))#初始化输出
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            if(mode == 'max'):
                Y[i,j] = X[i:i+p_h,j:j+p_w].max() #计算第i行到i+p_h行以及第j列到第j+p_w列所组成的矩阵的最大值作为输出Y[i, j]的值
            if(mode == 'avg'):
                Y[i, j] = X[i:i + p_h, j:j + p_w].mean() #计算第i行到i+p_h行以及第j列到第j+p_w列所组成的矩阵的平均值作为输出Y[i, j]的值
    return Y
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
print(pool2d(X, (2, 2))) #使用最大池化层
print(pool2d(X,(2,2),'avg')) #使用平均池化层

#池化层填充和步幅
X = torch.arange(16,dtype=torch.float32).reshape((1,1,4,4)) #输入是四维张量,(batch_size,chanels,h,w)
pool2d_max = nn.MaxPool2d(3) #深度学习框架中的步幅与汇聚窗口的大小相同,此处都为3
Y = pool2d_max(X) #输出也是四维张量,(batch_size,chanels,h,w)
print(Y)
pool2d_max1 = nn.MaxPool2d(kernel_size=3,padding=1,stride=2) #填充和步幅可以手动设定
print(pool2d_max1(X))
pool2d_max2 = nn.MaxPool2d(kernel_size=(2,3),stride=(2,3),padding=(0,1)) #设定一个任意大小的矩形汇聚窗口,并分别设定填充和步幅的高度和宽度
print(pool2d_max2(X))
#池化层多输入通道
X = torch.cat((X,X+1),dim=1) #在通道维度上连结张量X和X + 1(广播机制,在X中所有元素都加1),以构建具有2个通道的输入,dim=1表示通道维度上,0表示batch_size维度上
Y = pool2d_max1(X)
print(Y)#输出通道的数量跟输入通道的数量相同,也是2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值