2D卷积很适合处理图像输入。以之前的MNIST为例,在使用MLP时,为方便全连接层的处理,将28乘28的输入flatten成了784维的向量,这样就要有784个权值。而使用了2D卷积以后,假设使用3乘3的卷积核,那么每次的输入都是卷积核扫过的3乘3大小的区域,这样就只需要有9个权值,参数量大大减少了。
卷积操作:卷积核和扫过的小区域对应位置相乘再求和的操作,卷积完成后一般要加个偏置bias。一种Kernel如果分成多个通道上的子Kernel做卷积运算,最后运算结果还要加在一起后,再加偏置。
注意,在不同的文献中卷积核的叫法可能不同,Kernel、Filter、Weight指的都是卷积核。
Input Channels指输入图像的通道数(如灰度图像就是1通道,RGB图像忽略alpha通道后就是3通道)。
Kernel Channels不是一个值,它是两个值组成的一组值,表征了这个卷积核将输入图像从多少个通道映射到多少个通道上,如下(当然也可以反过来写):
( 和 I n p u t C h a n n e l s 相 等 的 通 道 数 , 用 了 多 少 种 卷 积 核 ) (和Input \ Channels相等的通道数,用了多少种卷积核) (和Input Channels相等的通道数,用了多少种卷积核)
这里说种其实是想说,实际用的卷积核数量是Input Channels乘以后面的这个"卷积核种数"。也就是说对于每种卷积核都按照输入通道数分成了不同通道上的卷积核,对于不同通道上的同种卷积核:它们不共享权值,但共享一个偏置。
Kernel Size指卷积核的长宽,如为3则卷积核是3乘3的。
Stride指每次移动卷积核的步长,显然这个值会大幅影响输出的Feature Map的shape。
Padding指为输入图像外围补充的圈数,注意如28乘28的图像,补充Padding=1就变成30乘30的了(而不是29乘29)。这个值一般直接在卷积时候定义,不必手动为输入图像加Padding。
nn.Conv2d
(类式接口)import torch
from torch import nn
"""2维的卷积层,用于图片的卷积"""
# 输入图像的通道数=1(灰度图像),卷积核的种类数=3
# 卷积核的shape是3乘3的,扫描步长为1,不加padding
layer = nn.Conv2d(1, 3, kernel_size=3, stride=1, padding=0)
"""要输入的原始图像"""
# 样本数=1,通道数=1,图像的shape是28乘28的
x = torch.rand(1, 1, 28, 28)
"""使用上面定义的卷积层layer和输入x,完成一次卷积的前向运算"""
out = layer.forward(x)
# 得到的还是1张图片,因为用了3种kernel所以输出的通道数变成3了
# 因为没加padding,原来28乘28的图像在3乘3卷积下得到的边长是28-3+1=26
print(out.shape) # torch.Size([1, 3, 26, 26])
"""添加padding看看"""
# 这次使用padding为1.所以原始图像上下左右都加了一层0
layer = nn.Conv2d(1, 3, kernel_size=3, stride=1, padding=1)
print(layer.forward(x).shape) # torch.Size([1, 3, 28, 28])
"""步长设置为2看看"""
# stride设置为2,也就是每次移动2格子(向上或者向右)
layer = nn.Conv2d(1, 3, kernel_size=3, stride=2, padding=1)
# 相当于每次跳1个像素地扫描,输出的Feature Map直接小了一半
print(layer.forward(x).shape) # torch.Size([1, 3, 14, 14])
"""实际使用时,应该这样用!"""
out = layer(x)
print(out.shape) # torch.Size([1, 3, 14, 14])
运行结果:
torch.Size([1, 3, 26, 26])
torch.Size([1, 3, 28, 28])
torch.Size([1, 3, 14, 14])
torch.Size([1, 3, 14, 14])
特别注意,在使用时应该直接使用layer(x)
而不是layer.forward(x)
,因为前者实际是调用了__call__()
,而PyTorch在这个函数中定义了一些hooks,如果要使用这些钩子的功能就只能用前者了!它会先运行hooks再运行.forward()
函数。
在前面定义的卷积层的基础上,查看一下卷积层的权重(即卷积核)信息和偏置信息:
print(layer.weight)
print(layer.bias)
运行结果:
tensor([[[[ 0.1277, -0.1672, 0.1102],
[ 0.3176, 0.0236, 0.2537],
[ 0.0737, 0.0904, 0.0261]]],
[[[ 0.0349, -0.2042, 0.1766],
[-0.0938, -0.0470, 0.2011],
[-0.2460, 0.0876, 0.3124]]],
[[[-0.2361, -0.0971, -0.1031],
[-0.0756, -0.3073, 0.3227],
[-0.1951, -0.2395, -0.0769]]]], requires_grad=True)
Parameter containing:
tensor([ 0.0790, -0.3261, 0.0697], requires_grad=True)
查看一下shape:
print(layer.weight.shape)
print(layer.bias.shape)
运行结果:
torch.Size([3, 1, 3, 3])
torch.Size([3])
F.conv2d
(函数式接口)PyTorch里一般小写的都是函数式的接口,相应的大写的是类式接口。函数式的更加low-level一些,如果不需要做特别复杂的配置只要用类式接口就够了。
可以这样理解:
nn.Conv2d
是[2D卷积层],而F.conv2d
是[2D卷积操作]。
import torch
from torch.nn import functional as F
"""手动定义卷积核(weight)和偏置"""
w = torch.rand(16, 3, 5, 5) # 16种3通道的5乘5卷积核
b = torch.rand(16) # 和卷积核种类数保持一致(不同通道共用一个bias)
"""定义输入样本"""
x = torch.randn(1, 3, 28, 28) # 1张3通道的28乘28的图像
"""2D卷积得到输出"""
out = F.conv2d(x, w, b, stride=1, padding=1) # 步长为1,外加1圈padding
print(out.shape)
out = F.conv2d(x, w, b, stride=2, padding=2) # 步长为2,外加2圈padding
print(out.shape)
运行结果:
torch.Size([1, 16, 26, 26])
torch.Size([1, 16, 14, 14])