我第一次看到卷积这个概念应该是在本科概率论的卷积公式里,但当时只会用,不理解含义,后面又在传统的图像处理算法以及深度学习中经常用到卷积,但不同处的卷积在概念上又有一些不同与相同之处,而综合之后对卷积也有了更深的理解,因此结合自身理解以及相关视频,基于深度学习对卷积整理如下。
主要参考自:王木头学科学、跟李沐学AI、霹雳吧啦Wz、小元老师、同济子豪兄、pytorch官网等。
1.从信号处理的角度理解卷积
下面来分析卷积的由来与物理意义。
假设一个人不停的吃东西,进食函数f(t)与时间t关系如下
除了吃,还得消化,假设消化与进食多少无关,则食物消化函数g(t)与时间t的关系(即任何一件吃进去的食物随时间变化所剩的比例)如下
问下午两点,这个人肚里还剩多少食物?
直接观察,比较难回答,因为吃和消化都在不停的变化,但利用卷积公式将模型抽象出来就很容易解决
分析:卷积公式包括函数f与函数g,这里将f函数表示进食,用g函数表示消化
-
假设不考虑消化,下午两点时,这个人肚子里有多少食物?
显然,将0点到14点吃的所有食物加起来即可
2.但吃进去的东西还在不停地消化,需考虑消化,则需要利用消化函数g
假设在12点吃了一碗米饭,该米饭在下午两点会被消化一部分,需要利用g函数计算消化量
即12点吃的食物f(12) * 随时间变化剩余食物的比例g(14-12)=米饭剩余量
同理,假设在8点吃了早餐,10点吃了面包,12点吃了米饭,可以计算得出食物的剩余量,计算公式为f(x)*g(t-x)
那么对0到t时刻所有吃下的食物的剩余量求和,即可得到t时刻食物剩余量,求和计算公式即为卷积公式
卷积公式的标志:函数f与函数g的变量相加,会抵消,只剩下一个变量,在图像上即对应函数f到函数g的一条连线
每条连线都对应着某时刻的f(t)*g(t-x),将每条线加起来即可得到所求的T时刻剩余食物量积分公式
就等价于卷积公式
由上可知卷积公式的物理意义:
对于一个系统,
如果输入不稳定(每时刻进食的食物量不确定),
输出是稳定的(不同食物经过相同时间的消化比例确定),
那么就可以利用卷积公式求系统存量
如果,我们将g函数翻转,则之前的连线图可得到另一幅图,因为翻转了,所以叫做卷积。
2.从图像卷积操作的角度理解卷积
卷积神经网络的主要应用是对图片里的内容进行识别,比如识别图片里面的猫、狗,而卷积神经网络之所以叫做卷积神经网络,是因为在将特征向量传到全连接层之前,会先对图片进行一系列的卷积操作,抽取特征。
那么能否借助卷积的意义去理解图像的卷积操作呢,卷积无非就是f和g两个函数,是不是只需要找到图像卷积里将什么当做不稳定输入函数 f和稳定输出函数 g就可以搞明白了呢?
图像卷积操作:将3x3(或别的尺寸)的卷积核扣在图片上,对应位置相乘,再将结果相加(图中右下角应该为-4)
将卷积核不断移动,即可得到卷积后的图像
此外,为了让图像大小不发生变化,会预先在图片周围加一层padding
直观上将图像看做函数f,卷积核看做函数g,但似乎无法理解,因为图像不是一个系统,没有不稳定的输入,也没有稳定的输出
需要跳出系统,对卷积有一个更广泛的理解
重新举一个蝴蝶效应的例子来扩展我们的理解
假设在t时刻发生了飓风,而发生飓风的原因,是由于在它之前,有很多蝴蝶扇动了翅膀,例如在x时刻,蝴蝶扇动翅膀会对t时刻的产生影响,而影响力会随时间t发生衰减。右图计算的即是,t时刻飓风发生时,之前的蝴蝶扇动翅膀对自己产生了多少影响。
也就是说卷积可以用来计算一件事发生时,之前的事情对当前事件的影响程度
如果将时间t换为距离d,那么可以类比于计算当前地点的发生的事件受周围事件的影响程度,且周围事件对当前事件的影响随距离的变化而发生变化。
对应到图像卷积操作,卷积操作可理解为计算周围的很多像素点如何对某一个像素点产生影响
比如传统图像处理中的平滑操作,即是利用这样一个卷积核对图像进行卷积操作,从而实现平滑处理
如图所示,平滑卷积就是在求平均值,即如果周围的像素点与该像素点相比数值较高,那么经过卷积求平均就会降低,反之就会升高。也就是说,卷积核实质上是规定了周围像素如何对当前像素产生影响的。
通过上述分析,我们确实可以把图片看做函数f,卷积核看做函数g,来进行卷积函数运算,是否真的是这样呢?
如上图所示,f(x-1, y-1)为x时刻,f(x, y)为t时刻,那么相差为x-(x-1)=1, y-(y-1)=1, 则对应的g为g(1, 1)
将其全部展开,可以看到位置对应上其实是反过来的
如果图像卷积操作对应实际的卷积运算,应该将卷积核旋转180度
也就是说,实际上,卷积核并不等于函数g,函数g需要旋转180度才等于卷积核。因此,严格来说,卷积层是个错误的叫法,图像中的卷积操作所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。
下面简单实现一个二维互相关运算并验证其正确性。
import torch
def corr2d(X, K):
"""计算二维互相关运算"""
h, w = K.shape
"""
因为卷积核的宽度和高度大于1,而卷积核只与图像中每个大小完全适合的位置进行,
互相关运算。所以,输出大小等于输入大小减去卷积核大小+1,
即(Nh-Kh+1)*(Nw-Kw+1)
"""
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 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() # 对应位置相乘并求和
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)
综上,图像卷积意义在于计算周围像素点产生的影响,而g函数即卷积核模板规定了如何影响。
此外,除了平滑卷积核,还有许多别的卷积核,如垂直卷积核与水平卷积核等等
如垂直卷积核计算时就会忽略横向的边界,而将垂直边界给挑选出来,这时,虽然依然是进行卷积操作,实际效果为将图片中的一些特征给挑选出来,而过滤掉其他的特征,这也即是传统图像处理中的滤波器的一种。
在这种情况下,它就是对周围像素点的一个主动的试探和选择,当你想重点关注某个位置时,可以特殊设置该位置卷积核的值,从而通过卷积核将周围有用的特征给保留下来,也就是说图像卷积可以用于提取局部特征。
下面简单用代码演示,如何利用固定的卷积核来找到像素变化的位置,从而来检测图像中不同颜色的边缘,实现边缘检测
# 构造一个像素的黑白图像。中间四列为黑色6X8,其余像素为白色
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
# 构造一个边缘检测的卷积核
K = torch.tensor([[1.0, -1.0]])
# 对输入X与卷积核K进行互相关运算
Y = corr2d(X, K)
Y
如上所示,输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为0
3.从图像卷积的角度理解神经网络中的卷积
从第二节的探讨中,我们知道图像卷积可以用于提取局部特征。因此从这个角度来看,可以很自然地在神经网络中利用卷积层来抽取局部特征,模拟从像素到局部特征再到整体特征,类似人脑的递进。
某种意义上来说,卷积神经网络是通过比较图像局部特征是否相同来判断图片类别的
如下图,判断手写的文字属于哪个字母,如果单纯对比每一个像素,计算机显然是无法识别手写体的
而卷积神经网络则可以通过对比提取出的局部特征是否相似,来给出图片属于哪一个类别的概率。
所以卷积神经网络的第一步就是利用卷积层将图片的局部特征提取出来,再把这些特征图交给神经网络的全连接层进行图片分类
而卷积神经网络与传统图像处理中的卷积操作不同在于,卷积核是可学的,而不是固定的模板,在卷积神经网络中,卷积核先进行随机初始化,再基于神经网络的反向传播,通过梯度下降不断优化,最终基于目标函数(损失函数)自动地训练出提取所需局部特征的卷积核。
相比于传统的机器学习方法,即通过人为设计一些固定的卷积核模板提取局部特征,再将这些特征向量传入如SVM、决策树之类的机器学习算法进行分类,传统方法由于特征是人为设计,往往无法设计出完全适用于算法语义空间的特征向量,从而效果欠佳,而深度卷积神经网络保持了特征提取的过程与后续推理过程在同一个特征语义空间内,使得提取出的特征能更有效地用于后续推理,这或许也是深度学习目前优于传统机器学习方法的原因之一。
4. 从神经网络的角度理解卷积神经网络中的卷积
回忆一下,最简单的神经网络结构如图所示
其中神经网络中全连接层(BP、前馈)公式为:
对于表格数据,我们寻找的模式可能涉及特征之间的交互,但是我们不能预先假设任何与特征交互相关的先验结构。 此时,多层感知机可能是最好的选择,然而对于高维感知数据,上述这种缺少结构的网络可能会变得不实用。
例如对于猫狗分类任务,假设我们使用相机(1200w像素)拍摄一张图片,RGB图片则有36M元素,即使只使用一层100大小的单隐藏层的MLP,模型也有3.6B(14GB)元素,已经远多于世界上所有猫和狗的总数(900M狗,600M猫),其参数过多极易出现过拟合且存储、训练神经网络的成本也极高,即将很小的图像作为输入,简单叠加几层,参数就会爆炸,显然传统的多层感知机不适于处理计算机视觉任务。
因此自然想到,可以设想利用图片中的一些先验结构来减少参数量并增强神经网络对于视觉任务的处理能力。
一般将这种先验结构称为归纳偏置。当这种偏置与现实相符时,就能得到样本有效的模型,并且这些模型能很好地泛化到未知数据中。
从二、三节的分析中,我们发现图像卷积可以有效地提取局部特征,下面进一步分析为什么卷积操作对于图片而言具有这种特性。
假设在图片中寻找某个物体:
无论哪种方法找到这个物体,都应该和物体的位置无关,比如狗无论在图片中的哪个位置,人眼都能识别出该物体是一只狗,即图片具有平移不变性。
另一方面在图片中离的比较远的两个像素往往没有什么关系,而离得近的两个像素相似度却很高,图片具有局部性。
综上可以得出图片结构的两大特性,并可将其用于设计适合于计算机视觉的神经网络:
-
平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性“
-
局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测
而卷积核的范围使得它只关心周围的像素(局部性),且卷积操作时是使用同一个卷积核在图片上滑动(平移不变性),这使得卷积操作天然地就符合图片的这两种特性。
而在进行卷积操作时,需要存储的参数就只有对应的卷积核,大大减少了参数量,因此,可以设法将神经网络中的MLP层替换为卷积层。
推导如下:
对其加上图片位置的索引
下面利用归纳偏置进行简化
至此,通过平移不变性:x的平移会导致h的平移,因此v不应该依赖于i,j
与局部性:评估h(i,j)时不应该用远离x(i,j)的参数,将全连接层变为了卷积层,其中V是卷积核。
下面基于上面定义的corr2d实现二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
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
在前面,我们提到,卷积核大小会导致卷积后的特征图大小发生变化,具体来说,我们可以通过填充和步幅控制卷积后的特征图大小(这个公式非常重要,需要牢记)
其中W为输入图片大小、F为卷积核大小、S为步幅大小、P为padding(填充)大小
此外,在卷积神经网络中由于卷积核是从数据中学习到的,因此无论这些层执行严格的卷积运算还是互相关运算(最后都会学习出相同的结果),卷积层的输出都不会受到影响。
最后,由于通道数的存在,二维图片的一组卷积核其实一个三维(h x w x c)的东西,如下图所示,通过卷积层从3通道变换到2通道,需要两组(对应输出的通道数)卷积核,每组卷积包含3个卷积核(对应输入的通道数),3个不同的卷积核对3个不同的输入通道进行卷积,再加和。 因为每个通道都向后续层提供一组空间化的学习特征,因此这些通道有时也被称为特征映射(feature maps),不同的输入通道对应不同的卷积核,这使得一些通道可以专门识别边缘,而一些通道可以专门识别纹理。
5. 基于pytorch实现Lenet网络并用于图片分类 (pytorch官网入门demo)
最简单的卷积神经网络训练所需:
1.model.py ——定义LeNet网络模型
2.train.py ——加载数据集并训练,训练集计算loss,测试集计算accuracy,保存训练好的网络参数
3.predict.py——得到训练好的网络参数后,用自己找的图像进行分类测试
Lenet 网络模型
# model.py
import torch.nn as nn
import torch.nn.functional as F
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels = 3, out_channels = 16, kernel_size = 5)
# in_channels:输入特征矩阵的深度
# out_channels:输入特征矩阵的深度
# kernel_size:卷积核的尺寸
# stride:步幅
self.pool1 = nn.MaxPool2d(kernel_size = 2, stride = 2)
self.conv2 = nn.Conv2d(16, 32, 5)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(32*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
x = F.relu(self.conv1(x)) # input(3, 32, 32) output(16, 28, 28)
x = self.pool1(x) # output(16, 14, 14)
x = F.relu(self.conv2(x)) # output(32, 10, 10)
x = self.pool2(x) # output(32, 5, 5)
x = x.view(-1, 32*5*5) # output(32*5*5)
x = F.relu(self.fc1(x)) # output(120)
x = F.relu(self.fc2(x)) # output(84)
x = self.fc3(x) # output(10)
return x
# train.py
import torch
import torchvision
import torch.nn as nn
from model import LeNet
import torch.optim as optim
import torchvision.transforms as transforms
def main():
# 数据预处理
transform = transforms.Compose(
[transforms.ToTensor(), #相当于把图片0-255的像素值归一化到0-1,防止引起数值问题
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 导入、加载 训练集,50000张训练图片
# download设置为True、自动下载数据集
train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=36,
shuffle=True, num_workers=0)
# 导入、加载 测试集,10000张验证图片
# download设置为True、自动去下载数据集
val_set = torchvision.datasets.CIFAR10(root='./data', train=False,
download=False, transform=transform)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=5000,
shuffle=False, num_workers=0)
val_data_iter = iter(val_loader)
val_image, val_label = val_data_iter.next()
# classes = ('plane', 'car', 'bird', 'cat',
# 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
net = LeNet() # 定义训练的网络模型
device = torch.device("cuda")
net.to(device) # 加载模型到GPU
loss_function = nn.CrossEntropyLoss() # 定义损失函数为交叉熵损失函数
optimizer = optim.Adam(net.parameters(), lr=0.001) # 定义优化器(训练参数,学习率)
for epoch in range(5): # loop over the dataset multiple times
running_loss = 0.0
for step, data in enumerate(train_loader, start=0):
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data # 获取训练集的图像和标签
# zero the parameter gradients
optimizer.zero_grad() # 清除历史梯度
# forward + backward + optimize
outputs = net(inputs.to(device)) # 正向传播
loss = loss_function(outputs, labels.to(device)) # 计算损失
loss.backward() # 反向传播
optimizer.step() # 优化器更新参数
# 打印训练、验证时间、损失、准确率等数据
running_loss += loss.item()
if step % 500 == 499: # 每500 mini-batches验证一次
with torch.no_grad():
outputs = net(val_image.to(device)) # [batch, 10]
predict_y = torch.max(outputs, dim=1)[1]
accuracy = torch.eq(predict_y, val_label.to(device)).sum().item() / val_label.size(0)
print('[%d, %5d] train_loss: %.3f test_accuracy: %.3f' %
(epoch + 1, step + 1, running_loss / 500, accuracy))
running_loss = 0.0
print('Finished Training')
# 保存模型
save_path = './Lenet.pth'
torch.save(net.state_dict(), save_path)
if __name__ == '__main__':
main()
# predict.py
# 导入包
import torch
import torchvision.transforms as transforms
from PIL import Image
from model import LeNet
# 数据预处理
transform = transforms.Compose(
[transforms.Resize((32, 32)), # 首先需resize成跟训练集图像一样的大小
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 导入要测试的图像(自己找的,不在数据集中),放在源文件目录下
im = Image.open('horse.jpg')
im = transform(im) # [C, H, W]
im = torch.unsqueeze(im, dim=0) # 对数据增加一个新维度,因为tensor的参数是[batch, channel, height, width]
# 实例化网络,加载训练好的模型参数
net = LeNet()
net.load_state_dict(torch.load('Lenet.pth'))
# 预测
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
with torch.no_grad():
outputs = net(im)
predict = torch.max(outputs, dim=1)[1].data.numpy()
print(classes[int(predict)])
总结:
-
LeNet是早期成功的神经网络
-
先使用卷积层来学习图片空间信息
-
然后使用全连接层来转换到类别空间
主要需要掌握pytorch如何定义一个最简单的卷积网络(在init中定义网络层,在forward中编写前向传播过程),pytorch导入官方数据集、数据预处理、实例化网络、如何训练网络进行梯度更新、准确度验证、保存与加载模型、预测等。