由浅入深,走进深度学习(补充篇:转置卷积和FCN)

发布于:2024-07-04 ⋅ 阅读:(12) ⋅ 点赞:(0)

本期内容是针对神经网络层结构的一个补充,主要内容是:转置卷积和全连接卷积网络

相关内容:

由浅入深,走进深度学习(2)_卷积层-CSDN博客

由浅入深,走进深度学习(补充篇:神经网络结构层基础)-CSDN博客

由浅入深,走进深度学习(补充篇:神经网络基础)-CSDN博客

目录

转置卷积

填充、步幅和多通道

全连接卷积神经网络

正片开始!!!!

转置卷积


卷积不会增大输入的高宽  通常要么不变  要么减半
转置卷积则可以用来增大输入高宽

import torch
from torch import nn
from d2l import torch as d2l

# 实现基本的转置卷积运算
# 定义转置卷积运算
def trans_conv(X, K):
    # 获取卷积核的宽度和高度
    h, w = K.shape # 卷积核的宽、高
    # 创建一个新的张量Y  其尺寸为输入X的尺寸加上卷积核K的尺寸减去1  在常规卷积中  输出尺寸通常是输入尺寸减去卷积核尺寸加1
    Y = torch.zeros((X.shape[0] + h -1, X.shape[1] + w - 1)) # 正常的卷积后尺寸为(X.shape[0] - h + 1, X.shape[1] - w + 1)       
    # 遍历输入张量X的每一行
    for i in range(X.shape[0]):
        # 遍历输入张量X的每一列
        for j in range(X.shape[1]):
            # 对于输入X的每一个元素  我们将其与卷积核K进行元素级别的乘法  然后将结果加到输出张量Y的相应位置上
            Y[i:i + h, j:j + w] += X[i, j] * K # 按元素乘法  加回到自己矩阵
    # 返回转置卷积的结果
    return Y

# 验证上述实现输出
# 定义输入张量X  这是一个2x2的矩阵
X = torch.tensor([[0.0, 1.0],
                  [2.0, 3.0]])
# 定义卷积核K  也是一个2x2的矩阵
K = torch.tensor([[0.0, 1.0],
                  [2.0, 3.0]])
# 调用上面定义的trans_conv函数  对输入张量X和卷积核K进行转置卷积操作  并打印结果
trans_conv(X, K)


# 使用高级API获得相同的结果
# 将输入张量X和卷积核K进行形状变换  原来是2x2的二维张量  现在变成了1x1x2x2的四维张量
# 第一个1表示批量大小  第二个1表示通道数  2x2是卷积核的高和宽
X, K = X.reshape(1, 1, 2, 2), K.reshape(1, 1, 2, 2)
# 创建一个转置卷积层对象tconv  其中输入通道数为1   输出通道数为1  卷积核的大小为2  没有偏置项
tconv = nn.ConvTranspose2d(1, 1, kernel_size = 2, bias = False) # 输入通道数为1  输出通道数为1   
# 将创建的转置卷积层对象tconv的权重设置为我们的卷积核K
tconv.weight.data = K
# 使用创建的转置卷积层tconv对输入张量X进行转置卷积操作  并返回结果
tconv(X)

填充、步幅和多通道

填充在输出上  padding=1  之前输出3x3  现在上下左右都填充了1  那就剩下中心那个元素了
填充为1  就是把输出最外面的一圈当作填充
创建一个转置卷积层对象tconv  其中输入通道数为1  输出通道数为1  卷积核的大小为2  没有偏置项  同时设置填充大小为1  
填充(padding)操作在输出上执行  原本输出为3x3  由于填充了大小为1的边框  结果就只剩下中心的元素   
所以填充大小为1就相当于将输出矩阵最外面的一圈当作填充并剔除

tconv = nn.ConvTranspose2d(1, 1, kernel_size = 2, padding = 1, bias = False) 
# 将创建的转置卷积层对象tconv的权重设置为我们的卷积核K
tconv.weight.data = K
# 使用创建的转置卷积层tconv对输入张量X进行转置卷积操作  并返回结果
tconv(X)


# 创建一个转置卷积层对象tconv  其中输入通道数为1  输出通道数为1  卷积核的大小为2  步幅(stride)为2  没有偏置项
# 步幅为2表示在进行卷积时  每次移动2个单位  相较于步幅为1  这样会使得输出尺寸增大
tconv = nn.ConvTranspose2d(1, 1, kernel_size = 2, stride = 2, bias = False)
# 将创建的转置卷积层对象tconv的权重设置为我们的卷积核K
tconv.weight.data = K
# 使用创建的转置卷积层tconv对输入张量X进行转置卷积操作 并返回结果
tconv(X)


# 多通道
# 创建一个四维张量X  批量大小为1  通道数为10  高和宽都为16
X = torch.rand(size = (1, 10, 16, 16))
# 创建一个二维卷积层对象conv  其中输入通道数为10  输出通道数为20  卷积核大小为5  填充为2  步幅为3
# 这会将输入的10个通道的图像转换为20个通道的特征图
conv = nn.Conv2d(10, 20, kernel_size = 5, padding = 2, stride = 3)
# 创建一个转置卷积层对象tconv  其中输入通道数为20  输出通道数为10  卷积核大小为5  填充为2  步幅为3
# 这会将输入的20个通道的特征图转换回10个通道的图像
tconv = nn.ConvTranspose2d(20, 10, kernel_size = 5, padding = 2, stride = 3)
# 首先对输入张量X进行卷积操作   然后再对卷积的结果进行转置卷积操作
# 然后检查这个结果的形状是否和原始输入张量X的形状相同
# 如果相同  说明转置卷积操作成功地还原了原始输入的形状
tconv(conv(X)).shape == X.shape


# 与矩阵变换的联系
# 创建一个一维张量  其中包含从0.0到8.0的连续数字
# 然后将这个一维张量重塑为3x3的二维张量
X = torch.arange(9.0).reshape(3, 3)
# 创建一个2x2的卷积核K  其中包含四个元素:1.0,2.0,3.0,4.0
K = torch.tensor([[1.0, 2.0],
                  [3.0, 4.0]])
# 使用自定义的二维卷积函数corr2d对输入张量X和卷积核K进行卷积操作
# corr2d函数需要在引入d2l(深度学习库)之后才能使用
Y = d2l.corr2d(X, K) # 卷积
# 打印卷积操作的结果
Y


# 定义一个函数kernel2matrix  用于将给定的卷积核K转换为一个稀疏矩阵W
def kernel2matrix(K):
    # 创建长度为5的零向量k和4x9的零矩阵W
    k, W = torch.zeros(5), torch.zeros((4,9))
    # 打印初始状态的k
    print(k) 
    # 打印初始状态的W
    print(W)
    # 打印输入的卷积核K
    print(K)
    # 将卷积核K的元素填充到向量k中的适当位置  形成一个稀疏向量
    k[:2], k[3:5] = K[0, :], K[1,:]
    # 打印填充后的向量k
    print(k)
    # 将稀疏向量k填充到矩阵W中的适当位置  形成一个稀疏矩阵
    W[0, :5], W[1, 1:6], W[2, 3:8], W[3, 4:] = k, k, k, k
    # 返回转换后的稀疏矩阵W
    return W

# 每一行向量表示在一个位置的卷积操作  0填充表示卷积核未覆盖到的区域。
# 输入大小为 3 * 3 的图片  拉长一维向量后变成 1 * 9 的向量
# 输入大小为 3 * 3 的图片  卷积核为 2 * 2  则输出图片为 2 * 2  拉长后变为 4 * 1 的向量
# kernel2matrix函数将卷积核改为稀疏矩阵C后矩阵情况

# 使用kernel2matrix函数将卷积核K转换为一个稀疏矩阵W
# 这个矩阵的每一行表示在一个特定位置进行的卷积操作  其中的0表示卷积核没有覆盖的区域
# 如果输入是一个3x3的图像  并被拉平为一个1x9的向量
# 而卷积核是2x2的  那么输出图像的大小为2x2  拉平后变为一个4x1的向量
# kernel2matrix函数实际上就是在构建这种转换关系
W = kernel2matrix(K) 
W
# 打印输入张量X的内容
print(X)
# 使用reshape函数将输入张量X拉平为一个一维向量   并打印结果
# 这是为了将X与稀疏矩阵W进行矩阵乘法操作
print(X.reshape(-1))
# 判断卷积操作的结果Y是否等于稀疏矩阵W与拉平的输入张量X的矩阵乘法的结果   并将结果重塑为2x2的形状
# 这是一种检查卷积操作是否等价于某种矩阵变换的方式
Y == torch.matmul(W, X.reshape(-1)).reshape(2, 2)


# 使用自定义的转置卷积函数trans_conv对卷积操作的结果Y和卷积核K进行转置卷积操作
Z = trans_conv(Y, K)
# 判断转置卷积操作的结果Z是否等于稀疏矩阵W的转置与拉平的卷积结果Y的矩阵乘法的结果   并将结果重塑为3x3的形状
# 这是一种检查转置卷积操作是否等价于某种特定的矩阵变换的方式
# 注意这里得到的结果并不是原图像   尽管它们的尺寸是一样的
Z == torch.matmul(W.T, Y.reshape(-1)).reshape(3, 3) # 由卷积后的图像乘以转置卷积后  得到的并不是原图像  而是尺寸一样    

全连接卷积神经网络


1 * 1 卷积层来降低维度
转置卷积层把图片扩大,k是通道有多少类,通道数为类别数,则导致可以对每一个像素分类

FCN是用深度神经网络来做语义分割的奠基性工作
其用转置卷积层来替换CNN最后的全连接层  从而可以实现每个像素的预测

# 全连接卷积神经网络
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

# 使用在ImageNet数据集上预训练的ResNet18模型来提取图像特征
pretrained_net = torchvision.models.resnet18(pretrained=True)
# 使用list函数和children方法列出预训练模型的所有子层(这些子层通常是神经网络的层)
# 然后使用Python的切片语法来取出最后三层
# 这可以帮助我们理解模型的结构  特别是在我们打算对模型进行微调或者使用模型的某些层来提取特征时
list(pretrained_net.children())[-3:] # 查看最后三层长什么样子

# 创建一个全卷积网络实例net
# 使用预训练的ResNet18模型创建一个新的神经网络
# 其中  "*list(pretrained_net.children())[:-2]"这段代码将ResNet18模型的所有子层(除了最后两层)作为新网络的层    
# 这样  新网络实际上是ResNet18模型去掉最后两层后的版本
# 这种方法常常用于迁移学习  即利用一个在大型数据集上训练过的模型的特征提取部分  来帮助我们处理新的任务
net = nn.Sequential(*list(pretrained_net.children())[:-2]) # 去掉ResNet18最后两层
# 创建一个形状为(1, 3, 320, 480)的随机张量  这可以看作是一张形状为(320, 480)  有三个颜色通道的图片
# 这里的3代表图片的颜色通道数量(红、绿、蓝)  320和480分别代表图片的高度和宽度
# 随机张量的所有元素都是在[0, 1)之间随机生成的  可以看作是随机图片的像素值
X = torch.rand(size=(1,3,320,480)) # 卷积核与输入大小无关  全连接层与输入大小有关
# 将随机生成的图片输入到网络中  通过调用net(X)  进行前向传播
# 打印输出张量的形状  输出的形状通常可以用于检查网络的结构是否正确
# 对于全卷积网络  输出的宽度和高度通常会比输入的小  这是由于卷积和池化操作造成的
# 在这个例子中  输出的宽度和高度应该是输入的1/32  这是由ResNet18的结构决定的
net(X).shape  # 缩小32倍


# 使用1X1卷积层将输出通道数转换为Pascal VOC2012数据集的类数(21类)
# 将要素地图的高度和宽度增加32倍
# 定义目标数据集中的类别数量  这里的21表示Pascal VOC2012数据集中有21个类别  包括20个物体类别和一个背景类别
num_classes = 21
# 在网络末尾添加一个新的卷积层  这是一个1x1的卷积层  输入通道数为512(这是由前面的ResNet18模型决定的)
# 输出通道数为我们定义的类别数量,即21
# 1x1卷积层常用于改变通道数  即可以将前一层的特征图投影到一个新的空间  这个新的空间的维度即为卷积层的输出通道数
net.add_module('final_conv',nn.Conv2d(512,num_classes,kernel_size=1))  
# 图片放大32倍  所以stride为32
# padding根据kernel要保证高宽不变的最小值  16 * 2 = 32  图片左右各padding
# kernel为64  原本取图片32大小的一半  再加上padding的32  就相当于整个图片
# 再添加一个转置卷积层  转置卷积也被称为反卷积  通常用于将小尺寸的特征图放大到原来的大小
# 这里的输入和输出通道数都是num_classes  表示我们希望在放大的过程中保持通道数不变
# kernel_size是64  stride是32  这意味着这一层将特征图的宽度和高度放大了32倍
# padding是16  它用于在特征图的边缘添加额外的区域  使得输出的大小正好是输入的32倍
net.add_module('transpose_conv', nn.ConvTranspose2d(num_classes,num_classes,kernel_size=64,padding=16,stride=32)) 
# 初始化转置卷积层

# 双线性插值核的实现
# 定义一个函数  用于初始化双线性插值核
# 这个函数接受三个参数:输入通道数、输出通道数和核大小
def bilinear_kernel(in_channels, out_channels, kernel_size):
    # 计算双线性插值核中心点位置
    # 计算双线性插值核的尺寸的一半  由于我们希望中心点位于核的中心  所以需要先计算核的一半大小
    # 我们使用 // 运算符进行整数除法  确保结果为整数
    factor = (kernel_size + 1) // 2
    # 根据核的大小是奇数还是偶数  确定中心点的位置
    # 如果核的大小是奇数  则中心点位于尺寸的一半减去1的位置  因为Python的索引从0开始  所以减去1
    # 例如  如果核的大小是3  那么中心点应该位于1的位置  (3+1)//2 - 1 = 1
    if kernel_size % 2 == 1:
        center = factor - 1
    # 如果核的大小是偶数  则中心点位于尺寸的一半减去0.5的位置
    # 这是因为偶数大小的核没有明确的中心点  所以我们取中间两个元素的平均位置作为中心点
    # 例如  如果核的大小是4   那么中心点应该位于1.5的位置  (4+1)//2 - 0.5 = 1.5
    else:
        center = factor - 0.5
    # 创建一个矩阵  其元素的值等于其与中心点的距离
    og = (torch.arange(kernel_size).reshape(-1,1),
         torch.arange(kernel_size).reshape(1,-1))
    # 计算双线性插值核  其值由中心点出发  向外线性衰减
    filt = (1 - torch.abs(og[0] - center) / factor) * (1 - torch.abs(og[1] - center) / factor)    
    # 初始化一个权重矩阵  大小为 (输入通道数, 输出通道数, 核大小, 核大小)
    weight = torch.zeros((in_channels, out_channels, kernel_size, kernel_size))  
    # 将双线性插值核的值赋给对应位置的权重
    weight[range(in_channels),range(out_channels),:,:] = filt
    # 返回初始化的权重矩阵  这个权重矩阵可以直接用于初始化转置卷积层的权重
    return weight

# 双线性插值的上采样实验
# 创建一个转置卷积层  输入和输出通道数都是3  这是因为我们处理的是RGB图片  每个颜色通道都需要进行处理
# 核大小是4  步长是2  这意味着这个层将输入的宽度和高度放大了2倍
# 设置bias为False  因为我们不需要偏置项
conv_trans = nn.ConvTranspose2d(3,3,kernel_size=4,padding=1,stride=2,bias=False)  
# 使用双线性插值核初始化转置卷积层的权重
# 这里我们使用了copy_方法  这是一种就地操作  直接修改了原始张量的值
conv_trans.weight.data.copy_(bilinear_kernel(3,3,4)) # 双线性核初始化权重
# 使用torchvision.transforms.ToTensor()将一张JPEG格式的图片转换为张量
img = torchvision.transforms.ToTensor()(d2l.Image.open('01_Data/03_catdog.jpg'))
# 增加一个批次维度
X = img.unsqueeze(0)
# 将图片张量输入到转置卷积层中  得到上采样的结果
Y = conv_trans(X)
# 将输出结果转换为可以展示的格式   即(高度, 宽度, 颜色通道)的格式  并从计算图中分离出来  这样就可以转换为NumPy数组
out_img = Y[0].permute(1,2,0).detach()
# 设置展示图片的大小
d2l.set_figsize()
# 打印输入图片的形状
print('input image shape:', img.permute(1,2,0).shape)
# 展示图片
d2l.plt.imshow(img.permute(1,2,0))
# 打印输出图片的形状并展示图片
# 可以看到  输出图片的宽度和高度都是输入的2倍  这正是我们设置的步长
print('output image shape:',out_img.shape) # 输出被拉大了2倍
# 并展示图片
d2l.plt.imshow(out_img)


# 用双线性插值的上采样初始化转置卷积层
# 对于1X1卷积层   我们使用Xavier初始化参数
# 使用双线性插值核初始化转置卷积层的权重
# num_classes是目标数据集中的类别数量   这里使用双线性插值核的尺寸为64
W = bilinear_kernel(num_classes, num_classes, 64)
# 将初始化的权重W复制给转置卷积层的权重
# 使用copy_方法进行就地操作  直接修改原始张量的值
net.transpose_conv.weight.data.copy_(W)

注:上述内容参考b站up主“我是土堆”的视频,参考吴恩达深度学习,机器学习内容,参考李沐动手学深度学习!!!


网站公告

今日签到

点亮在社区的每一天
去签到