当前位置:   article > 正文

Pytorch:网络层介绍(卷积层、池化层、线性层、激活函数层)和多层感知机_pytorch 网络层

pytorch 网络层

一、卷积层—Convolution Layers

卷积的定义

在数学中,两个函数(比如 f , g : R d → R f, g: \mathbb{R}^d \to \mathbb{R} f,g:RdR)之间的“卷积”被定义为

( f ∗ g ) ( x ) = ∫ f ( z ) g ( x − z ) d z . (f * g)(\mathbf{x}) = \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}. (fg)(x)=f(z)g(xz)dz.

也就是说,卷积是当把一个函数“翻转”并移位 x \mathbf{x} x时,测量 f f f g g g之间的重叠。
当为离散对象时,积分就变成求和。例如,对于由索引为 Z \mathbb{Z} Z的、平方可和的、无限维向量集合中抽取的向量,我们得到以下定义:

( f ∗ g ) ( i ) = ∑ a f ( a ) g ( i − a ) . (f * g)(i) = \sum_a f(a) g(i-a). (fg)(i)=af(a)g(ia).

对于二维张量,则为 f f f的索引 ( a , b ) (a, b) (a,b) g g g的索引 ( i − a , j − b ) (i-a, j-b) (ia,jb)上的对应加和:

( f ∗ g ) ( i , j ) = ∑ a ∑ b f ( a , b ) g ( i − a , j − b ) . (f * g)(i, j) = \sum_a\sum_b f(a, b) g(i-a, j-b). (fg)(i,j)=abf(a,b)g(ia,jb).

卷积在图像处理领域的作用简单来说,卷积运算就是卷积核在图像上滑动, 相应位置上进行乘加。 卷积过程类似于用一个卷积核去图像上寻找与它相似的区域, 与卷积核模式越相似则激活值越高, 从而实现特征提取。

1.1 1d / 2d / 3d卷积

即便上述对于卷积的定义还算规范,但是依旧不清楚卷积到底做了什么,下面便以1d / 2d / 3d卷积作为示范,看看卷积操作是怎么进行的吧。

卷积维度:一般情况下,卷积核在几个维度上滑动,就是几维卷积

(1)1d卷积示意图

在这里插入图片描述

(2)2d卷积示意图

在这里插入图片描述

(2)3d卷积示意图

在这里插入图片描述

1.2 卷积—nn.Conv2d()

nn.Conv2d

功能: 对多个二维信号进行二维卷积
在这里插入图片描述

其中主要参数的功能如下表示:

参数作用
in_channels输入通道数
out_channels输出通道数, 等价于卷积核个数
kernel_size卷积核尺寸, 这个代表着卷积核的大小
stride步长, 这个指的卷积核滑动的时候,每一次滑动几个像素。
padding填充个数,用于检测边缘部分的像素
dilation孔洞卷积大小

首先根据卷积的定义可以知道,在只讨论一条轴的情况下,输出维度等于输入维度 - 卷积核维度 + 1。举个栗子:输入的高度和宽度都为 3 3 3,卷积核的高度和宽度都为 2 2 2,则生成的输出表征的维数为 2 × 2 2\times2 2×2
总结一下就是:假设输入形状为 n h × n w n_h\times n_w nh×nw,卷积核形状为 k h × k w k_h\times k_w kh×kw,那么输出形状将是 ( n h − k h + 1 ) × ( n w − k w + 1 ) (n_h-k_h+1) \times (n_w-k_w+1) (nhkh+1)×(nwkw+1)

因此,卷积的输出形状取决于输入形状和卷积核的形状。

关于padding的概念:通常,如果我们添加 p h p_h ph行填充(大约一半在顶部,一半在底部)和 p w p_w pw列填充(左侧大约一半,右侧一半),则输出形状将为 ( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) 。 (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1)。 (nhkh+ph+1)×(nwkw+pw+1)这意味着输出的高度和宽度将分别增加 p h p_h ph p w p_w pw

在许多情况下,我们需要设置 p h = k h − 1 p_h=k_h-1 ph=kh1 p w = k w − 1 p_w=k_w-1 pw=kw1使输入和输出具有相同的高度和宽度

关于stride的概念:通常,当垂直步幅为 s h s_h sh、水平步幅为 s w s_w sw时,输出形状为 ⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / s w ⌋ . \lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor. ⌊(nhkh+ph+sh)/sh×⌊(nwkw+pw+sw)/sw.如果我们设置了 p h = k h − 1 p_h=k_h-1 ph=kh1 p w = k w − 1 p_w=k_w-1 pw=kw1,则输出形状将简化为 ⌊ ( n h + s h − 1 ) / s h ⌋ × ⌊ ( n w + s w − 1 ) / s w ⌋ \lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor ⌊(nh+sh1)/sh×⌊(nw+sw1)/sw
更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 ( n h / s h ) × ( n w / s w ) (n_h/s_h) \times (n_w/s_w) (nh/sh)×(nw/sw)

上述的数学表达可能有些抽象,下面看下动图来理解步长的概念:左图的步长是1, 所以每一次滑动1个像素;而右图的步长是2,所以每一次滑动2个像素。


下面依然用一个动图展示来理解padding: 首先看左图,这个是没有padding的卷积,做卷积运算后会发现这种情况卷积后的输出图像会从4 × \times × 4变为3 × \times × 3;然后看右图,这是padding=1的情况的填充方式,做卷积运算后可以发现输出图像和原图像均为5 × \times × 5。


孔洞卷积就可以理解成一个带孔的卷积核, 常用于图像分割任务,主要功能就是提高感受野。也就是输出图像的一个参数,能看到前面图像更大的一个区域。下面是动图的演示:


下面便是测试代码,用来看看卷积的效果:


# 工具包的函数定义如下:

import torch
import random
import numpy as np
from PIL import Image
import torchvision.transforms as transforms


def transform_invert(img_, transform_train):
    """
    将data 进行反transfrom操作
    :param img_: tensor
    :param transform_train: torchvision.transforms
    :return: PIL image
    """
    if 'Normalize' in str(transform_train):
        norm_transform = list(filter(lambda x: isinstance(x, transforms.Normalize), transform_train.transforms))
        mean = torch.tensor(norm_transform[0].mean, dtype=img_.dtype, device=img_.device)
        std = torch.tensor(norm_transform[0].std, dtype=img_.dtype, device=img_.device)
        img_.mul_(std[:, None, None]).add_(mean[:, None, None])

    img_ = img_.transpose(0, 2).transpose(0, 1)  # C*H*W --> H*W*C
    if 'ToTensor' in str(transform_train):
        img_ = img_.detach().numpy() * 255

    if img_.shape[2] == 3:
        img_ = Image.fromarray(img_.astype('uint8')).convert('RGB')
    elif img_.shape[2] == 1:
        img_ = Image.fromarray(img_.astype('uint8').squeeze())
    else:
        raise Exception("Invalid img shape, expected 1 or 3 in axis 2, but got {}!".format(img_.shape[2]) )

    return img_


def set_seed(seed=1): # 设置随机种子
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

在这里插入图片描述
改一下代码中的随机种子后如下图所示:
在这里插入图片描述
由此呢,可以发现随机种子的设置代表了不同权重的卷积核,而不同权重的卷积核代表不同的模式(比如锐化、模糊等卷积核),从而会关注不同的特征进而提出。

然后打印一下处理前后图像尺寸以验证上文关于padding的解释:
在这里插入图片描述
可以看到卷积后的尺寸变小了,我们再看回到卷积核尺寸定义的地方,会发现卷积核的尺寸为3 × \times × 3,且padding为0,stride也为0,如图所示:
在这里插入图片描述
因此根据卷积尺度的计算公式呢是可以验证的。

然后呢依然是以断点调试为工具,看看卷积层有哪些参数:由于上一节的学习中已经知道了卷积层也是继承nn.Module的,因此必然后8个字典参数,这次主要看看_modules参数和_parameters参数字典。
首先在图示位置处打上断点,然后Debug进入,运行完断点处的代码后即可查看:
在这里插入图片描述
由于图示中定义的卷积层没有子模块,所以_modules为空,但由于初始化了模型的可学习参数偏置bias和权重weight,于是_parameters非空。进一步可以看到weight的形状是[1, 3, 3, 3],这里从左往右解释:1代表着卷积核的个数,第1个3表示的输入通道数(也就是RBG通道数),后面两个3是二维卷积核的尺寸。 这里便涉及了用三维卷积核做二维卷积的问题了,为此先有如下定义:

多输入通道的卷积运算

当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为 c i c_i ci,那么卷积核的输入通道数也需要为 c i c_i ci。如果卷积核的窗口形状是 k h × k w k_h\times k_w kh×kw,那么当 c i = 1 c_i=1 ci=1时,我们可以把卷积核看作形状为 k h × k w k_h\times k_w kh×kw的二维张量。

然而,当 c i > 1 c_i>1 ci>1时,我们卷积核的每个输入通道将包含形状为 k h × k w k_h\times k_w kh×kw的张量。将这些张量 c i c_i ci连结在一起可以得到形状为 c i × k h × k w c_i\times k_h\times k_w ci×kh×kw的卷积核。由于输入和卷积核都有 c i c_i ci个通道,我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将 c i c_i ci的结果相加)得到二维张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。

  • 输入 X : c i × n h × c w X:c_{i}×n_{h}×c_{w} X:ci×nh×cw
  • W : c i × k h × k w W:c_{i}×k_{h}×k_{w} W:ci×kh×kw
  • 输出 Y : m h × m w Y = ∑ i = 0 c i X i , : , : ∗ W i , : , : Y:m_{h}×m_{w} \\ Y={\textstyle \sum_{i=0}{c_{i}}} X_{i,:,:}*W_{i,:,:} Y:mh×mwY=i=0ciXi,:,:Wi,:,:

下图便是具有两个输入通道的二维互相关运算的示例。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素: ( 1 × 1 + 2 × 2 + 4 × 3 + 5 × 4 ) + ( 0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 ) = 56 (1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56 (1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56

在这里插入图片描述
回到我们的demo当中,我们的图像是3通道(RGB)的图像,这时需要创建3个二维的卷积核来分别对应一个通道进行卷积,然后这三个二维卷积核处理后得到的输出图像对应位置的三个数之和加上偏置才是一个输出结果。 也就是说我们用三个二维卷积核做卷积运算,然后再将这三个结果接在一起就成了三维结果了,而第三个维度就是卷积核的个数。 说到这可能还有有些晕乎乎的,下面用一个GIF来演示一下吧(RGB彩色图像的卷积过程(gif动图演示)), 下面每一块扫描都是对应元素相乘再相加得到最后的的结果:

既然已经说到了卷积,那就再补充几个特殊形式的卷积运算吧:

多输出通道的卷积运算

每一层有多个输出通道在实际应用中是至关重要的。在最流行的神经网络架构中,随着神经网络层数的加深,常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。用人话说就是,我们可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。

  • 每个输出通过可以识别特定模式
  • 输入通道核识别并组合输入中的模式

c i c_i ci c o c_o co分别表示输入和输出通道的数目,并让 k h k_h kh k w k_w kw为卷积核的高度和宽度。为了获得多个通道的输出,可以为每个输出通道创建一个形状为 c i × k h × k w c_i\times k_h\times k_w ci×kh×kw的卷积核张量,这样卷积核的形状是 c o × c i × k h × k w c_o\times c_i\times k_h\times k_w co×ci×kh×kw。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。

1 × 1 1\times 1 1×1 卷积层

1 × 1 1\times 1 1×1 卷积层不识别空间信息,只是融合通道,相当于输入形状为 n h n w × c i n_{h}n_{w}×c_{i} nhnw×ci,权重为 c o × c i c_{o}×c_{i} co×ci的全连接层(o和i分别表示输出通道数和输入通道数)

1 × 1 1 \times 1 1×1卷积,即 k h = k w = 1 k_h = k_w = 1 kh=kw=1,看起来似乎没有多大意义。
毕竟,卷积的本质是有效提取相邻像素间的相关特征,而 1 × 1 1 \times 1 1×1卷积显然没有此作用。尽管如此, 1 × 1 1 \times 1 1×1仍然十分流行,经常包含在复杂深层网络的设计中。下面详细地解读一下它的实际作用。

因为使用了最小窗口, 1 × 1 1\times 1 1×1卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。其实 1 × 1 1\times 1 1×1卷积的唯一计算发生在通道上。

下图展示了使用 1 × 1 1\times 1 1×1卷积核与 3 3 3个输入通道和 2 2 2个输出通道的互相关计算。这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。可以将 1 × 1 1\times 1 1×1卷积层看作在每个像素位置应用的全连接层,以 c i c_i ci个输入值转换为 c o c_o co个输出值。因为这仍然是一个卷积层,所以跨像素的权重是一致的。同时, 1 × 1 1\times 1 1×1卷积层需要的权重维度为 c o × c i c_o\times c_i co×ci,再额外加上一个偏置。

在这里插入图片描述

1.3 转置卷积—nn.ConvTranspose

转置卷积

卷积神经网络层,例如卷积层和汇聚层,通常会减少下采样输入图像的空间维度(高和宽)。然而如果输入和输出图像的空间维度相同,在以像素级分类的语义分割中将会很方便。例如,输出像素所处的通道维可以保有输入像素在同一位置上的分类结果。

为了实现这一点,尤其是在空间维度被卷积神经网络层缩小后,可以使用另一种类型的卷积神经网络层,它可以增加上采样中间层特征图的空间维度。转置卷积便可以用于逆转下采样导致的空间尺寸减小。

首先从最基本的情况入手,暂时忽略通道,设步幅为1且没有填充。假设我们有一个 n h × n w n_h \times n_w nh×nw的输入张量和一个 k h × k w k_h \times k_w kh×kw的卷积核。
以步幅为1滑动卷积核窗口,每行 n w n_w nw次,每列 n h n_h nh次,共产生 n h n w n_h n_w nhnw个中间结果。每个中间结果都是一个 ( n h + k h − 1 ) × ( n w + k w − 1 ) (n_h + k_h - 1) \times (n_w + k_w - 1) (nh+kh1)×(nw+kw1)的张量,初始化为0。为了计算每个中间张量,输入张量中的每个元素都要乘以卷积核,从而使所得的 k h × k w k_h \times k_w kh×kw张量替换中间张量的一部分。请注意,每个中间张量被替换部分的位置与输入张量中元素的位置相对应。最后,所有中间结果相加以获得最终结果。下图解释了如何为 2 × 2 2\times 2 2×2的输入张量计算卷积核为 2 × 2 2\times 2 2×2的转置卷积。

在这里插入图片描述

与常规卷积不同,在转置卷积中,填充被应用于的输出(常规卷积将填充应用于输入)。例如,当将高和宽两侧的填充数指定为1时,转置卷积的输出中将删除第一和最后的行与列。而步幅被指定为中间结果(输出),而不是输入。
且步幅在常规卷积用于减少输出图像的维度,但在转置卷积中可以增加输出图像的维度,具体演示如下图所示:

在这里插入图片描述
实际上,转置卷积可以利用矩阵变换来实现。抽象来看,给定输入向量 x \mathbf{x} x(矩阵 X \mathbf{X} X拉伸后的向量)和权重矩阵 W \mathbf{W} W,卷积的前向传播函数可以通过将其输入与权重矩阵相乘并输出向量 y \mathbf{y} y(矩阵 Y \mathbf{Y} Y拉伸后的向量)= W x \mathbf{W}\mathbf{x} Wx来实现。由于反向传播遵循链式法则和 ∇ x y = W ⊤ \nabla_{\mathbf{x}}\mathbf{y}=\mathbf{W}^\top xy=W,卷积的反向传播函数可以通过将其输入与转置的权重矩阵 W ⊤ \mathbf{W}^\top W相乘来实现。因此,转置卷积层能够交换卷积层的正向传播函数和反向传播函数:它的正向传播和反向传播函数将输入向量分别与 W ⊤ \mathbf{W}^\top W W \mathbf{W} W相乘。

测试代码如下:

# 转置卷积与矩阵变换的关系

import torch
from torch import  nn

# 自定义卷积运算函数
def corr2d(X, K):  #@save
    """计算二维互相关运算"""
    h, w = K.shape
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # 输出的维度为 n-k+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() # * 表示点积
            # Y[i,j] = (X[i:i+h,j:j+w]*K).sum()
    return Y

# 自定义转置卷积运算函数
def trans_conv(X, K):
    h, w = K.shape
    # Kernel的维度:h × w
    Y = torch.zeros((X.shape[0] + h - 1, X.shape[1] + w - 1))
    # Y的维度为:X的行 + h - 1 × X的列 + w -1
    for i in range(X.shape[0]):# 对每一个X[i, j]做按元素乘法
        for j in range(X.shape[1]):
            Y[i:i + h, j:j + w] += X[i, j] * K
    return Y

# 实现自定义转置卷积
X = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
trans_conv(X, K)

# 调用高级API来实现转置卷积
X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2)
tconv = nn.ConvTranspose2d(1, 1, kernel_size=2, bias=False) # 参数分别为:输入通道数,输出通道数,Kernel大小,偏差
tconv.weight.data = K
tconv(X)

# ---------------------------------------------------------------------------------------------
# 验证转置卷积和矩阵变换
X = torch.arange(9.0).reshape(3, 3)
K = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
Y = corr2d(X, K) # Y是cov(X,K)

# 将卷积核变成矩阵,4是Y=2*2拉伸的结果,9是X=3*3拉伸后的结果,
# 构造该矩阵的原理就是,首先确定矩阵的维度,然后卷积核扫过的每一个窗口对应X序号的值填入矩阵
# 此例中就是,卷积核第一次扫过的是X的左上角的2*2窗口,此时卷积核在X中对应位置的值为[[1,2,0],[3,4,0],[0,0,0]]
# 将次矩阵拉成向量后便是[1,2,0,3,4,0,0,0,0],这边是矩阵W对应的第一行
def kernel2matrix(K):
    k, W = torch.zeros(5), torch.zeros((4, 9))
    k[:2], k[3:5] = K[0, :], K[1, :]
    W[0, :5], W[1, 1:6], W[2, 3:8], W[3, 4:] = k, k, k, k
    return W
W = kernel2matrix(K)

# 判断矩阵乘法和卷积操作结果是否一致
print(Y == torch.matmul(W, X.reshape(-1)).reshape(2, 2)) # X.reshape(-1)是9 × 1的向量

# Z是自定义转置卷积的结果
Z = trans_conv(Y, K)

# 判断自定义转置卷积和矩阵变换的结果是否一致
print(Z == torch.matmul(W.T, Y.reshape(-1)).reshape(3, 3))

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

也许小伙伴们还是懵的,简单来说转置卷积是一个上采样用来增加输出图像的维度,看下面示意图:

接下来看看转置卷积是怎么在torch.nn的相关实现吧。

nn.ConvTranspose2d

功能:转置卷积实现上采样

参数作用
in_channels输入通道数
out_channels输出通道数, 等价于卷积核个数
kernel_size卷积核尺寸, 这个代表着卷积核的大小
stride步长, 用以增加输出图像维度。
padding填充个数,用以减小输出图像维度,例如当将高和宽两侧的填充数指定为1时,转置卷积的输出中将删除第一和最后的行与列。
dilation孔洞卷积大小
bias偏置

转置卷积的尺寸计算(卷积运算的尺寸逆):
在这里插入图片描述
测试代码

import torch
import random
import numpy as np
from PIL import Image
import torchvision.transforms as transforms


def transform_invert(img_, transform_train):
    """
    将data 进行反transfrom操作
    :param img_: tensor
    :param transform_train: torchvision.transforms
    :return: PIL image
    """
    if 'Normalize' in str(transform_train):
        norm_transform = list(filter(lambda x: isinstance(x, transforms.Normalize), transform_train.transforms))
        mean = torch.tensor(norm_transform[0].mean, dtype=img_.dtype, device=img_.device)
        std = torch.tensor(norm_transform[0].std, dtype=img_.dtype, device=img_.device)
        img_.mul_(std[:, None, None]).add_(mean[:, None, None])

    img_ = img_.transpose(0, 2).transpose(0, 1)  # C*H*W --> H*W*C
    if 'ToTensor' in str(transform_train):
        img_ = img_.detach().numpy() * 255

    if img_.shape[2] == 3:
        img_ = Image.fromarray(img_.astype('uint8')).convert('RGB')
    elif img_.shape[2] == 1:
        img_ = Image.fromarray(img_.astype('uint8').squeeze())
    else:
        raise Exception("Invalid img shape, expected 1 or 3 in axis 2, but got {}!".format(img_.shape[2]) )

    return img_


def set_seed(seed=1): # 设置随机种子
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)

import os
import torch.nn as nn
from PIL import Image
from torchvision import transforms
from matplotlib import pyplot as plt
# from tools.common_tools import transform_invert, set_seed

set_seed(5)  # 设置随机种子,不同的随机种子代表了卷积层的权重;设置后可以保证每次运行的结果一致

# ================================= load img ==================================
# 可以换成自己想要的图片
path_img = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lena.png") # os.path.dirname(os.path.abspath(__file__)))的作用是打印绝对路径
img = Image.open(path_img).convert('RGB')  # 0~255
# 将图片转化为Tensor张量
img_transform = transforms.Compose([transforms.ToTensor()])
img_tensor = img_transform(img)
img_tensor.unsqueeze_(dim=0)    # C*H*W to B*C*H*W,C表示channel数,H和W表示宽高,B表示批量大小

# ================================= create convolution layer ==================================
# ================ 2d卷积
flag = 0 # 标志变量
# flag = 0
if flag:
    # 定义卷积层
    conv_layer = nn.Conv2d(3, 1, 3)   # input:(i, o, size) weights:(o, i , h, w)
    nn.init.xavier_normal_(conv_layer.weight.data)# 权重初始化
    # 计算卷积
    img_conv = conv_layer(img_tensor)

# ================ transposed
# flag = 1
flag = 1
if flag:
    # 定义反卷积
    conv_layer = nn.ConvTranspose2d(3, 1, 3, stride=2)   # input:(i, o, size)
    nn.init.xavier_normal_(conv_layer.weight.data)

    # calculation
    img_conv = conv_layer(img_tensor)


# ================================= visualization ==================================
print("卷积前尺寸:{}\n卷积后尺寸:{}".format(img_tensor.shape, img_conv.shape))
img_conv = transform_invert(img_conv[0, 0:1, ...], img_transform)
img_raw = transform_invert(img_tensor.squeeze(), img_transform)
plt.subplot(122).imshow(img_conv, cmap='gray')
plt.subplot(121).imshow(img_raw)
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

在这里插入图片描述
关于转置卷积的棋盘效应,感兴趣的读者可以参阅Deconvolution and Checkerboard Artifacts

在这里插入图片描述
输入图像是512的, 卷积核大小是3,stride=2, 按照计算公式,输出尺寸: ( 512 − 1 ) × 2 + 3 = 1025 , ( 512 − 1 ) × 2 + 3 = 1025 ( 512 − 1 ) × 2 + 3 = 1025 ( 512 − 1 ) × 2 + 3 = 1025 ,(512-1)\times2+3=1025(512−1)×2+3=1025 (5121)×2+3=1025(5121)×2+3=1025(5121)×2+3=1025

二、池化层—Pooling Layer

池化运算:对信号进行“收集”并“总结”, 类似水池收集水资源, 所以池化层。

  • 收集: 多变少,图像的尺寸由大变小
  • 总结: 最大值/平均值
    具体可以看下图所示:

在这里插入图片描述

类似于上方提到的卷积运算中卷积核的移动,池化时的移动也是类似的,下面是一个最大池化的动态图(平均池化就是这些元素去平均值作为最终值):

(1)nn.MaxPool2d

功能:对二维信号(图像)进行最大值池化

参数作用
kernel_size卷积核尺寸
stride步长
padding填充个数
dilation池化间隔大小
ceil_mode尺寸向上取整,默认为False
return_indices记录池化像素索引

注意:stride一般设置的与窗口大小一致,以避免重叠

import torch
import random
import numpy as np
from PIL import Image
import torchvision.transforms as transforms


def transform_invert(img_, transform_train):
    """
    将data 进行反transfrom操作
    :param img_: tensor
    :param transform_train: torchvision.transforms
    :return: PIL image
    """
    if 'Normalize' in str(transform_train):
        norm_transform = list(filter(lambda x: isinstance(x, transforms.Normalize), transform_train.transforms))
        mean = torch.tensor(norm_transform[0].mean, dtype=img_.dtype, device=img_.device)
        std = torch.tensor(norm_transform[0].std, dtype=img_.dtype, device=img_.device)
        img_.mul_(std[:, None, None]).add_(mean[:, None, None])

    img_ = img_.transpose(0, 2).transpose(0, 1)  # C*H*W --> H*W*C
    if 'ToTensor' in str(transform_train):
        img_ = img_.detach().numpy() * 255

    if img_.shape[2] == 3:
        img_ = Image.fromarray(img_.astype('uint8')).convert('RGB')
    elif img_.shape[2] == 1:
        img_ = Image.fromarray(img_.astype('uint8').squeeze())
    else:
        raise Exception("Invalid img shape, expected 1 or 3 in axis 2, but got {}!".format(img_.shape[2]) )

    return img_


def set_seed(seed=1): # 设置随机种子
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)

import os
import torch.nn as nn
from PIL import Image
from torchvision import transforms
from matplotlib import pyplot as plt
# from tools.common_tools import transform_invert, set_seed

set_seed(1)  # 设置随机种子,不同的随机种子代表了卷积层的权重;设置后可以保证每次运行的结果一致

# ================================= load img ==================================
# 可以换成自己想要的图片
path_img = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lena.png") # os.path.dirname(os.path.abspath(__file__)))的作用是打印绝对路径
img = Image.open(path_img).convert('RGB')  # 0~255
# 将图片转化为Tensor张量
img_transform = transforms.Compose([transforms.ToTensor()])
img_tensor = img_transform(img)
img_tensor.unsqueeze_(dim=0)    # C*H*W to B*C*H*W,C表示channel数,H和W表示宽高,B表示批量大小

# ================ maxpool

flag = 1
if flag:
    maxpool_layer = nn.MaxPool2d((2, 2), stride=(2, 2))   # input:(i, o, size) weights:(o, i , h, w)
    img_pool = maxpool_layer(img_tensor)
    img_conv = img_pool

# ================ avgpool
# flag = 1
flag = 0
if flag:
    avgpoollayer = nn.AvgPool2d((2, 2), stride=(2, 2))   # input:(i, o, size) weights:(o, i , h, w)
    img_pool = avgpoollayer(img_tensor)
    img_conv = img_pool

# ================================= visualization ==================================
print("卷积前尺寸:{}\n卷积后尺寸:{}".format(img_tensor.shape, img_conv.shape))
img_conv = transform_invert(img_conv[0, 0:3, ...], img_transform)
img_raw = transform_invert(img_tensor.squeeze(), img_transform)
plt.subplot(122).imshow(img_conv, cmap='gray')
plt.subplot(121).imshow(img_raw)
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

有同学可能会发现,咦怎么图片变成彩色的了?这个问题要回到代码部分了,其中值得一提的是,img_conv.shape的结果是一个四维张量,维度是批量大小×通道数×宽×高。自定义的transform_invert函数是转置卷积函数,返回PIL imgae,相比如卷积操作(卷积是用于特征提取),maxpooling中直接取用了img_conv[0, 0:3, ...],即RBG三个通道,但是卷积操作的时候只取了一个通道。最大池化的结果如下图所示:

在这里插入图片描述

基本上没差别,但是尺寸却变为了一半, 所以池化层是可以帮助我们剔除冗余像素以减少后面网络层的计算量。

(2)nn.AvgPool2d

除了最大池化,还有一个叫做平均池化:
功能:对二维信号(图像)进行平均值池化

参数作用
kernel_size卷积核尺寸
stride步长
padding填充个数
dilation池化间隔大小
count_include_pad填充值用于计算
divisor_override除法因子(自定义分母)

测试代码只需要将最大池化层Maxpooling中的标志变量flag修改一下即可,运行结果如下图所示:
在这里插入图片描述
很难看出最大值池化和平均池化有什么差别,但是按照二者的处理过程来说,最大池化的亮度会高些,毕竟它都是取的最大值,而平均池化是取平均值。

(3)nn.MaxUnpool2d

功能:对二维信号(图像)进行最大值池化上采样(反池化:将大尺寸图像变为小尺寸图像)

在这里插入图片描述

参数作用
kernel_size卷积核尺寸
stride步长
padding填充个数

参数与池化层是类似的。唯一的不同在于前向传播的时候需要传进一个indices,否则反池化后机器不知道应该把输入的元素放在哪个位置上。如下图所示,只有有了对应的索引后,才能确定输出的位置:

在这里插入图片描述
测试代码

import torch
import random
import numpy as np
from PIL import Image
import torchvision.transforms as transforms


def transform_invert(img_, transform_train):
    """
    将data 进行反transfrom操作
    :param img_: tensor
    :param transform_train: torchvision.transforms
    :return: PIL image
    """
    if 'Normalize' in str(transform_train):
        norm_transform = list(filter(lambda x: isinstance(x, transforms.Normalize), transform_train.transforms))
        mean = torch.tensor(norm_transform[0].mean, dtype=img_.dtype, device=img_.device)
        std = torch.tensor(norm_transform[0].std, dtype=img_.dtype, device=img_.device)
        img_.mul_(std[:, None, None]).add_(mean[:, None, None])

    img_ = img_.transpose(0, 2).transpose(0, 1)  # C*H*W --> H*W*C
    if 'ToTensor' in str(transform_train):
        img_ = img_.detach().numpy() * 255

    if img_.shape[2] == 3:
        img_ = Image.fromarray(img_.astype('uint8')).convert('RGB')
    elif img_.shape[2] == 1:
        img_ = Image.fromarray(img_.astype('uint8').squeeze())
    else:
        raise Exception("Invalid img shape, expected 1 or 3 in axis 2, but got {}!".format(img_.shape[2]) )

    return img_


def set_seed(seed=1): # 设置随机种子
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)

import os
import torch.nn as nn
from PIL import Image
from torchvision import transforms
from matplotlib import pyplot as plt
# from tools.common_tools import transform_invert, set_seed

set_seed(1)  # 设置随机种子,不同的随机种子代表了卷积层的权重;设置后可以保证每次运行的结果一致

# ================================= load img ==================================
# 可以换成自己想要的图片
path_img = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lena.png") # os.path.dirname(os.path.abspath(__file__)))的作用是打印绝对路径
img = Image.open(path_img).convert('RGB')  # 0~255
# 将图片转化为Tensor张量
img_transform = transforms.Compose([transforms.ToTensor()])
img_tensor = img_transform(img)
img_tensor.unsqueeze_(dim=0)    # C*H*W to B*C*H*W,C表示channel数,H和W表示宽高,B表示批量大小

# ================ max unpool
# flag = 1
flag = 1
if flag:
    # pooling
    img_tensor = torch.randint(high=5, size=(1, 1, 4, 4), dtype=torch.float)
    maxpool_layer = nn.MaxPool2d((2, 2), stride=(2, 2), return_indices=True)

    # 记录索引位置
    img_pool, indices = maxpool_layer(img_tensor)

    # unpooling
    img_reconstruct = torch.randn_like(img_pool, dtype=torch.float)
    maxunpool_layer = nn.MaxUnpool2d((2, 2), stride=(2, 2))

    # 传入索引
    img_unpool = maxunpool_layer(img_reconstruct, indices)

    # 打印索引信息
    print("raw_img:\n{}\nimg_pool:\n{}".format(img_tensor, img_pool))
    print("img_reconstruct:\n{}\nimg_unpool:\n{}".format(img_reconstruct, img_unpool))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79

测试代码的结果如下:
在这里插入图片描述

在这里插入图片描述

三、线性层—Linear Layer

线性层又称为全连接层,其每个神经元与上一层所有神经元相连实现对前一层的线性组合,线性变换

线性层的具体计算过程在下文中的多层感知(MLP)会详细解释,在此不多赘述。

nn.Linear

功能: 对一维信号(向量)进行线性组合

在这里插入图片描述

参数作用
in_features输入节点数
out_features输出节点数
bias是否需要偏置
# ================ linear
import torch
from torch import nn

inputs = torch.tensor([[1., 2, 3]])
linear_layer = nn.Linear(3, 4)
linear_layer.weight.data = torch.tensor([[1., 1., 1.],
                                         [2., 2., 2.],
                                         [3., 3., 3.],
                                         [4., 4., 4.]])

linear_layer.bias.data.fill_(0.5)
output = linear_layer(inputs)
print(inputs, inputs.shape)
print(linear_layer.weight.data, linear_layer.weight.data.shape)
print(output, output.shape)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

代码运行结果:

在这里插入图片描述

四、激活函数层—Activate Layer

激活函数Udine特征进行非线性变换, 赋予多层神经网络具有深度的意义。后边在多层感知机模块会详细介绍激活函数层是怎么实现赋予多层神经网络深度意义的。

(1)nn.Sigmoid

在这里插入图片描述

(2)nn.tanh

在这里插入图片描述

(3)nn.ReLU

在这里插入图片描述

(4)nn.LeakyReLU

在这里插入图片描述
可以在torch.nn中通过对应的激活函数名字直接调用。

五、多层感知机(MLP)

前面介绍了解一下线性层(全连接层)。然而深度学习主要关注多层模型,线性层只是一个单层神经网络。下面以多层感知机(multilayer perceptron,MLP)为例,了解多层神经网络。

隐藏层

多层感知机在单层神经网络的基础上引入了一到多个隐藏层(hidden layer)。隐藏层位于输入层和输出层之间。如下图所示,这是一个多层感知机的神经网络图。

在这里插入图片描述

在上图所示的多层感知机中,输入和输出个数分别为4和3,中间的隐藏层中包含了5个隐藏单元(hidden unit)。由于输入层不涉及计算,图中的多层感知机的层数为2。隐藏层中的神经元和输入层中各个输入完全连接,输出层中的神经元和隐藏层中的各个神经元也完全连接。因此,多层感知机中的隐藏层和输出层都是全连接层。

具体来说,给定一个小批量样本 X ∈ R n × d \boldsymbol{X} \in \mathbb{R}^{n \times d} XRn×d,其批量大小为 n n n,输入个数为 d d d。假设多层感知机只有一个隐藏层,其中隐藏单元个数为 h h h。记隐藏层的输出(也称为隐藏层变量或隐藏变量)为 H \boldsymbol{H} H,有 H ∈ R n × h \boldsymbol{H} \in \mathbb{R}^{n \times h} HRn×h。因为隐藏层和输出层均是全连接层,可以设隐藏层的权重参数和偏差参数分别为 W h ∈ R d × h \boldsymbol{W}_h \in \mathbb{R}^{d \times h} WhRd×h b h ∈ R 1 × h \boldsymbol{b}_h \in \mathbb{R}^{1 \times h} bhR1×h,输出层的权重和偏差参数分别为 W o ∈ R h × q \boldsymbol{W}_o \in \mathbb{R}^{h \times q} WoRh×q b o ∈ R 1 × q \boldsymbol{b}_o \in \mathbb{R}^{1 \times q} boR1×q

先看看一种含单隐藏层的多层感知机的设计。其输出 O ∈ R n × q \boldsymbol{O} \in \mathbb{R}^{n \times q} ORn×q的计算为
H = X W h + b h , O = H W o + b o ,

H=XWh+bh,O=HWo+bo,
HO=XWh+bh,=HWo+bo,也就是将隐藏层的输出直接作为输出层的输入。如果将以上两个式子联立起来,可以得到
O = ( X W h + b h ) W o + b o = X W h W o + b h W o + b o . \boldsymbol{O} = (\boldsymbol{X} \boldsymbol{W}_h + \boldsymbol{b}_h)\boldsymbol{W}_o + \boldsymbol{b}_o = \boldsymbol{X} \boldsymbol{W}_h\boldsymbol{W}_o + \boldsymbol{b}_h \boldsymbol{W}_o + \boldsymbol{b}_o. O=(XWh+bh)Wo+bo=XWhWo+bhWo+bo.从联立后的式子可以看出,虽然神经网络引入了隐藏层,却依然等价于一个单层神经网络:其中输出层权重参数为 W h W o \boldsymbol{W}_h\boldsymbol{W}_o WhWo,偏差参数为 b h W o + b o \boldsymbol{b}_h \boldsymbol{W}_o + \boldsymbol{b}_o bhWo+bo。不难发现,即便再添加更多的隐藏层,以上设计依然只能与仅含输出层的单层神经网络等价。

还记得前面提到过的激活函数可以赋予多层神经网络具有深度的意义了吗?上述问题的根源在于全连接层只是对数据做仿射变换(affine transformation),而多个仿射变换的叠加仍然是一个仿射变换。解决该问题的一个方法是引入非线性变换,例如对隐藏变量使用按元素运算的非线性函数进行变换,然后再作为下一个全连接层的输入。这个非线性函数被称为激活函数(activation function)。常用的几个激活函数已经在前文说过了,读者忘记了的话可以看回去哦。

MLP总结

总结一下呢就是:多层感知机是含有至少一个隐藏层的由全连接层组成的神经网络,且每个隐藏层的输出通过激活函数进行变换。多层感知机的层数和各隐藏层中隐藏单元个数都是超参数。以单隐藏层为例并沿用本节之前定义的符号,多层感知机按以下方式计算输出:
H = ϕ ( X W h + b h ) , O = H W o + b o ,

H=ϕ(XWh+bh),O=HWo+bo,
HO=ϕ(XWh+bh),=HWo+bo,其中 ϕ \phi ϕ表示激活函数。在分类问题中,我们可以对输出 O \boldsymbol{O} O做softmax运算,并使用softmax回归中的交叉熵损失函数。在回归问题中,将输出层的输出个数设为1,并将输出 O \boldsymbol{O} O直接提供给线性回归中使用的平方损失函数。

六、总结

这节内容主要是基础,简单的梳理一下,首先我们上次知道了构建神经网络的两个步骤:搭建子模块和拼接子模块。 而这次就是学习各个子模块的使用。 第一块内容是从比较重要的卷积层开始, 学习了1维/2维/3维卷积到底在干什么事情,采用了动图的方式进行演示, 卷积运算其实就是通过不同的卷积核去提取不同的特征。 然后学习了Pytorch的二维卷积运算及转置卷积运算,并进行了对比和分析了代码上如何实现卷积操作。第二块是池化运算和池化层的学习,然后了解了,最后是非线性激活函数。第三块就是了解一下多层感知机的概念,具体实现可以参考李沫大神的课程,里面的代码非常详细,这也是一个简单的应用,最好能做到手撕的哦。

参考资料

Pytorch教程
系统学习Pytorch笔记五
动手学深度学习

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/194247
推荐阅读
相关标签
  

闽ICP备14008679号