上一篇移步【动手学深度学习PyTorch版】11 使用GPU_水w的博客-CSDN博客
目录
一、卷积层
1.1从全连接到卷积
◼ 回顾单隐藏层MLP
◼ Waldo在哪里?
◼ 原则1-平移不变性
◼ 原则2-局部性
◼ 总结
1.2 卷积层
◼ 二维交叉相关
◼ 二维卷积层
◼ 交叉相关和卷积
◼ 一维和三维交叉相关
◼ 总结
二、代码实现图像卷积
◼ 互相关运算
◼ 二维卷积层
假设我们用手机拍了1200万像素----12M像素的图片,而且是RGB图片,那么就是3600万像素----36M像素。
假设我用一个单隐藏层的MLP来训练,隐藏层的大小为100的话,那么这个模型有3.6亿个元素,远远多于世界上所有的猫和狗的总数。那么模型还不如直接记住世界上所有的猫和狗的总数呢。
这对于网络来说,是一个问题。
隐藏层的大小100,那么权重就是100x3600万个,把这么多参数存下来的话,需要14GB。那么我如果用单隐藏层就会需要14GB的内存,这还没设计到做运算。
找的话,有两个原则:
那么,我们怎么样从全连接层出发,利用这两个原则,得到卷积?
我们之前单隐藏层的MLP来训练时,虽然一个图片是一个矩阵,但是是将输入做成一个一位的向量了。
现在我们还是还原成一个矩阵,因为我们要考虑空间的一些信息,所以我们还是将输入和输出变形成矩阵(宽度,高度)。
那么我们对应的可以将权重W变形成为一个四维的张量。之前是一个输入长度到输出长度的变化,现在是一个输入的宽度和高度到输出的宽度和高度的变化。
接下来,对W重新做一个的索引,把W的元素重新排列一下作用到v上面。使得
现在,我们看一下还有什么问题?
hij是通过x算出来的,那么假设x的位置发生变化的话,比如x平移,对应Vij会导致 hij 的平移,整个hij也会发生变化。但是V不应该依赖于(i,j),即i和j变化不应该会引起V发生变化。
所以直接得到了这个式子,我们一般叫做二维卷积,这是个误会,严格意义上来说,它是二维的交叉相关。
i和j这个输入,也就是我输出里面的像素,是等于以我输入的i和j对应像素为中心, 不断做offset,加一点减一点,往边上挪的时候,和模式Va,b做内积。
所以现在我们可以认为,二维卷积就是全连接或者矩阵乘法,但是权重使得它的一些东西是重复的,即不是每一个元素都可以自由变换。
我们需要知道,当我把一个模型的取值范围做了限制的时候,我就相当于吧这个模型的复杂度降低了,也就意味着我不需要存储那么多元素了。
假设我要计算hi,j这个输出的话,
这个式子中,对于Xi+a,j+b来说,a和b是变量。
但是实际上来说,我们不应该去看那么远的地方,i和j的结果只应该由输入附近的那些点就可以了。
我们可以做一些限制,解决方案就是:我在i和j那个点,离我当超过代尔塔的时候,就使得Va,b=0,不看V。
那就是说,我要做下面这个式子求和的时候,只需要计算i对于a从负达尔塔到达尔塔,j对于b从负达尔塔到达尔塔的局部性的变换。
卷积是一个特殊的全连接层,是weight shared全连接。
写成一个二维的输入和输出,对权重重新进行索引,
① 当在图片中形成一个识别器后,在一定像素大小的范围内,它都有自己的权重,当这个识别器在图片上换位置之后,它的权重应该不变。
② 理解成用同一张卷积核遍历整张图片。卷积核不会随着位置变化而变化。
③ 权重就是特征提取器,不应该随位置而发生变化。
④ 简而言之卷积核就是个框,在图片上不断扫描,无论扫在图上的哪个位置,卷积核都是不变的。
⑤ 对于一张图片应该有多个卷积核,但是每个卷积核要识别的东西不同,一个卷积核就是一个分类器。
⑥ 卷积确实是weight shared,但不是全联接,每个神经元是对应卷积核大小个输入。
⑦ 卷积就是weight shared全连接。
假设输入是一个 3x3 的矩阵,卷积核W(我们一般叫做Kernel)是一个2x2,其实就是说达尔塔等于1,那么输出就是:
当计算完19,计算25的时候,输出往右移了一位,那么输入也会往右移一位,那么对应的窗口就会发生变化,但核是不变的,满足平移不变性。而且输出只使用了2x2的窗口,满足局部性原则。
也就是说,一个核窗口,不断的在输入上往左移右移,上移下,来扫描几遍,得到我们的输出,这就是我们的二维交叉相关。
输出会变小,是因为在窗口移动的过程中,输入不足的时候,就会在输出的时候会丢掉一些东西。
我们可以从这个例子看出,不同的卷积核的值可以带来不同的效果,
比如说第一个:中间的8是一个比较大的值,边上的-1是一个比较小的值,会得到边缘检测的效果,把边缘值高亮出来。
我们可以认为是说,我的是神经网络可以去学习不同的核,来得到我想要的输出。
交叉相关和卷积其实没有太多区别,唯一的区别就是卷积的式子中有个负号。但是由于对称性,所以在实际的使用当中没有区别。
就相当于是说,如果二维交叉相关的W学出来是一个东西的话,那么二维卷积的W由于负号学出来的应该是一个反着的东西,反一下,就可以和二维交叉相关得到同样的效果。
一维,三维交叉相关和二维交叉相关其实本质上是一样的。
核卷积矩阵的大小控制局部性。
# 互相关运算
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): # X 为输入,K为核矩阵
"""计算二维互相关信息"""
h, w = K.shape # 核矩阵的行数和列数
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # X.shape[0]为输入高 , X.shape[1]为输入宽
for i in range(Y.shape[0]): # 遍历所有的Yij做计算
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum() # 图片的小方块区域[i:i + h, j:j + w],与卷积核做点积
return Y
# 验证上述二维互相关运算的输出
X = torch.tensor([[0.0,1.0,2.0],[3.0,4.0,5.0],[6.0,7.0,8.0]])
K = torch.tensor([[0.0,1.0],[2.0,3.0]])
corr2d(X,K)
验证上述二维互相关运算的输出,
与图中的结果一一对应。
# 实现二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
self.weight = nn.Parameter(torch.rand(kernel_size)) # 取随机值
self.bias = nn.Parameter(torch.zeros(1)) # 随机变量
def forward(Self, x):
return corr2d(x, self.weight) + self.bias # 用X和weight做互相关运算,再加上bias
(1)卷积层的一个简单应用:检测图片中不同颜色的边缘。
给定一个6x8的输入,把中间四列设置为0,用0 与 1 之间进行过渡,表示边缘。
要想把边缘检测出来,我们做一个1x2的核K,输出的Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘。
当然,卷积核K只能检测到垂直的边缘。
比如说,我们把X.t() 为X的转置,而K卷积核只能检测垂直边缘。所以结果全部为0。
# 卷积层的一个简单应用:检测图片中不同颜色的边缘
X = torch.ones((6,8))
X[:,2:6] = 0 # 把中间四列设置为0
print(X) # 0 与 1 之间进行过渡,表示边缘
# 做一个1x2的核
K = torch.tensor([[1.0,-1.0]]) # 如果左右原值相等,那么这两原值乘1和-1相加为0,则不是边缘
Y = corr2d(X, K)
print(Y)
print(corr2d(X.t(), K)) # X.t() 为X的转置,而K卷积核只能检测垂直边缘
(2)那么接下来,给定我们的输入X和输出Y,我们去学习卷积核K。
给定一个6x8的输入,把中间四列设置为0,用0 与 1 之间进行过渡,表示边缘。
我们想把边缘检测出来,并且给定的输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘。
我们应该怎么去学习这个1x2的卷积核K?
"""学习由X生成Y的卷积核"""
# 单个矩阵,输入通道为1,黑白图片通道为1,彩色图片通道为3。所以这里输入通道为1,输出通道为1.
# 去学习1x2的卷积核K
# 不需要bais
conv2d = nn.Conv2d(1, 1, kernel_size=(1,2), bias=False)
# 给X添加2个维度,通道维:通道数,RGB图3通道,灰度图1通道;批量维就是样本维,就是样本数
X = X.reshape((1,1,6,8))
Y = Y.reshape((1,1,6,7))
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2 # 使用均方误差作为loss
conv2d.zero_grad()
l.sum().backward()
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad # 梯度下降,3e-2是学习率
if(i+1) % 2 == 0: # 每2个batch就打印loss
print(f'batch {i+1},loss {l.sum():.3f}')
# 所学的卷积核的权重张量
print(conv2d.weight.data.reshape((1,2)))
可以看到,学习出来的这个1x2的卷积核K的值,基本上就和我们之前手动构造出来的[1,-1]差别不大。
这就是我们最简单的卷积层的定义,输入和输出通道都为1,没有做任何的填充和步幅。